From c527fd1f14c27855a37f2e8ac5346ce8d940ced2 Mon Sep 17 00:00:00 2001 From: Tudor Florea Date: Thu, 16 Oct 2014 03:05:19 +0200 Subject: initial commit for Enea Linux 4.0-140929 Migrated from the internal git server on the daisy-enea-point-release branch Signed-off-by: Tudor Florea --- bitbake/lib/bb/ui/__init__.py | 17 + bitbake/lib/bb/ui/buildinfohelper.py | 964 +++++++++++++ bitbake/lib/bb/ui/crumbs/__init__.py | 17 + bitbake/lib/bb/ui/crumbs/builddetailspage.py | 437 ++++++ bitbake/lib/bb/ui/crumbs/builder.py | 1475 ++++++++++++++++++++ bitbake/lib/bb/ui/crumbs/buildmanager.py | 455 ++++++ bitbake/lib/bb/ui/crumbs/hig/__init__.py | 0 .../lib/bb/ui/crumbs/hig/advancedsettingsdialog.py | 341 +++++ bitbake/lib/bb/ui/crumbs/hig/crumbsdialog.py | 44 + .../lib/bb/ui/crumbs/hig/crumbsmessagedialog.py | 70 + bitbake/lib/bb/ui/crumbs/hig/deployimagedialog.py | 219 +++ .../lib/bb/ui/crumbs/hig/imageselectiondialog.py | 172 +++ .../lib/bb/ui/crumbs/hig/layerselectiondialog.py | 298 ++++ .../lib/bb/ui/crumbs/hig/parsingwarningsdialog.py | 163 +++ bitbake/lib/bb/ui/crumbs/hig/propertydialog.py | 437 ++++++ bitbake/lib/bb/ui/crumbs/hig/proxydetailsdialog.py | 90 ++ .../lib/bb/ui/crumbs/hig/retrieveimagedialog.py | 51 + bitbake/lib/bb/ui/crumbs/hig/saveimagedialog.py | 159 +++ bitbake/lib/bb/ui/crumbs/hig/settingsuihelper.py | 122 ++ .../lib/bb/ui/crumbs/hig/simplesettingsdialog.py | 894 ++++++++++++ bitbake/lib/bb/ui/crumbs/hobcolor.py | 38 + bitbake/lib/bb/ui/crumbs/hobeventhandler.py | 639 +++++++++ bitbake/lib/bb/ui/crumbs/hoblistmodel.py | 903 ++++++++++++ bitbake/lib/bb/ui/crumbs/hobpages.py | 128 ++ bitbake/lib/bb/ui/crumbs/hobwidget.py | 904 ++++++++++++ bitbake/lib/bb/ui/crumbs/imageconfigurationpage.py | 561 ++++++++ bitbake/lib/bb/ui/crumbs/imagedetailspage.py | 669 +++++++++ bitbake/lib/bb/ui/crumbs/packageselectionpage.py | 355 +++++ bitbake/lib/bb/ui/crumbs/persistenttooltip.py | 186 +++ bitbake/lib/bb/ui/crumbs/progress.py | 23 + bitbake/lib/bb/ui/crumbs/progressbar.py | 59 + bitbake/lib/bb/ui/crumbs/puccho.glade | 606 ++++++++ bitbake/lib/bb/ui/crumbs/recipeselectionpage.py | 335 +++++ bitbake/lib/bb/ui/crumbs/runningbuild.py | 551 ++++++++ bitbake/lib/bb/ui/crumbs/sanitycheckpage.py | 85 ++ bitbake/lib/bb/ui/crumbs/utils.py | 34 + bitbake/lib/bb/ui/depexp.py | 326 +++++ bitbake/lib/bb/ui/goggle.py | 121 ++ bitbake/lib/bb/ui/hob.py | 109 ++ bitbake/lib/bb/ui/icons/images/images_display.png | Bin 0 -> 6898 bytes bitbake/lib/bb/ui/icons/images/images_hover.png | Bin 0 -> 7051 bytes bitbake/lib/bb/ui/icons/indicators/add-hover.png | Bin 0 -> 1212 bytes bitbake/lib/bb/ui/icons/indicators/add.png | Bin 0 -> 1176 bytes bitbake/lib/bb/ui/icons/indicators/alert.png | Bin 0 -> 3954 bytes .../lib/bb/ui/icons/indicators/confirmation.png | Bin 0 -> 5789 bytes bitbake/lib/bb/ui/icons/indicators/denied.png | Bin 0 -> 3955 bytes bitbake/lib/bb/ui/icons/indicators/error.png | Bin 0 -> 6482 bytes bitbake/lib/bb/ui/icons/indicators/info.png | Bin 0 -> 3311 bytes bitbake/lib/bb/ui/icons/indicators/issues.png | Bin 0 -> 4549 bytes bitbake/lib/bb/ui/icons/indicators/refresh.png | Bin 0 -> 5250 bytes .../lib/bb/ui/icons/indicators/remove-hover.png | Bin 0 -> 2809 bytes bitbake/lib/bb/ui/icons/indicators/remove.png | Bin 0 -> 1971 bytes bitbake/lib/bb/ui/icons/indicators/tick.png | Bin 0 -> 4563 bytes bitbake/lib/bb/ui/icons/info/info_display.png | Bin 0 -> 4117 bytes bitbake/lib/bb/ui/icons/info/info_hover.png | Bin 0 -> 4167 bytes bitbake/lib/bb/ui/icons/layers/layers_display.png | Bin 0 -> 4840 bytes bitbake/lib/bb/ui/icons/layers/layers_hover.png | Bin 0 -> 5257 bytes .../lib/bb/ui/icons/packages/packages_display.png | Bin 0 -> 7011 bytes .../lib/bb/ui/icons/packages/packages_hover.png | Bin 0 -> 7121 bytes bitbake/lib/bb/ui/icons/recipe/recipe_display.png | Bin 0 -> 4723 bytes bitbake/lib/bb/ui/icons/recipe/recipe_hover.png | Bin 0 -> 4866 bytes .../lib/bb/ui/icons/settings/settings_display.png | Bin 0 -> 6076 bytes .../lib/bb/ui/icons/settings/settings_hover.png | Bin 0 -> 6269 bytes .../bb/ui/icons/templates/templates_display.png | Bin 0 -> 5651 bytes .../lib/bb/ui/icons/templates/templates_hover.png | Bin 0 -> 5791 bytes bitbake/lib/bb/ui/knotty.py | 550 ++++++++ bitbake/lib/bb/ui/ncurses.py | 373 +++++ bitbake/lib/bb/ui/puccho.py | 425 ++++++ bitbake/lib/bb/ui/toasterui.py | 292 ++++ bitbake/lib/bb/ui/uievent.py | 133 ++ bitbake/lib/bb/ui/uihelper.py | 100 ++ 71 files changed, 14930 insertions(+) create mode 100644 bitbake/lib/bb/ui/__init__.py create mode 100644 bitbake/lib/bb/ui/buildinfohelper.py create mode 100644 bitbake/lib/bb/ui/crumbs/__init__.py create mode 100755 bitbake/lib/bb/ui/crumbs/builddetailspage.py create mode 100755 bitbake/lib/bb/ui/crumbs/builder.py create mode 100644 bitbake/lib/bb/ui/crumbs/buildmanager.py create mode 100644 bitbake/lib/bb/ui/crumbs/hig/__init__.py create mode 100644 bitbake/lib/bb/ui/crumbs/hig/advancedsettingsdialog.py create mode 100644 bitbake/lib/bb/ui/crumbs/hig/crumbsdialog.py create mode 100644 bitbake/lib/bb/ui/crumbs/hig/crumbsmessagedialog.py create mode 100644 bitbake/lib/bb/ui/crumbs/hig/deployimagedialog.py create mode 100644 bitbake/lib/bb/ui/crumbs/hig/imageselectiondialog.py create mode 100644 bitbake/lib/bb/ui/crumbs/hig/layerselectiondialog.py create mode 100644 bitbake/lib/bb/ui/crumbs/hig/parsingwarningsdialog.py create mode 100644 bitbake/lib/bb/ui/crumbs/hig/propertydialog.py create mode 100644 bitbake/lib/bb/ui/crumbs/hig/proxydetailsdialog.py create mode 100644 bitbake/lib/bb/ui/crumbs/hig/retrieveimagedialog.py create mode 100644 bitbake/lib/bb/ui/crumbs/hig/saveimagedialog.py create mode 100644 bitbake/lib/bb/ui/crumbs/hig/settingsuihelper.py create mode 100644 bitbake/lib/bb/ui/crumbs/hig/simplesettingsdialog.py create mode 100644 bitbake/lib/bb/ui/crumbs/hobcolor.py create mode 100644 bitbake/lib/bb/ui/crumbs/hobeventhandler.py create mode 100644 bitbake/lib/bb/ui/crumbs/hoblistmodel.py create mode 100755 bitbake/lib/bb/ui/crumbs/hobpages.py create mode 100644 bitbake/lib/bb/ui/crumbs/hobwidget.py create mode 100644 bitbake/lib/bb/ui/crumbs/imageconfigurationpage.py create mode 100755 bitbake/lib/bb/ui/crumbs/imagedetailspage.py create mode 100755 bitbake/lib/bb/ui/crumbs/packageselectionpage.py create mode 100644 bitbake/lib/bb/ui/crumbs/persistenttooltip.py create mode 100644 bitbake/lib/bb/ui/crumbs/progress.py create mode 100644 bitbake/lib/bb/ui/crumbs/progressbar.py create mode 100644 bitbake/lib/bb/ui/crumbs/puccho.glade create mode 100755 bitbake/lib/bb/ui/crumbs/recipeselectionpage.py create mode 100644 bitbake/lib/bb/ui/crumbs/runningbuild.py create mode 100644 bitbake/lib/bb/ui/crumbs/sanitycheckpage.py create mode 100644 bitbake/lib/bb/ui/crumbs/utils.py create mode 100644 bitbake/lib/bb/ui/depexp.py create mode 100644 bitbake/lib/bb/ui/goggle.py create mode 100755 bitbake/lib/bb/ui/hob.py create mode 100644 bitbake/lib/bb/ui/icons/images/images_display.png create mode 100644 bitbake/lib/bb/ui/icons/images/images_hover.png create mode 100644 bitbake/lib/bb/ui/icons/indicators/add-hover.png create mode 100644 bitbake/lib/bb/ui/icons/indicators/add.png create mode 100644 bitbake/lib/bb/ui/icons/indicators/alert.png create mode 100644 bitbake/lib/bb/ui/icons/indicators/confirmation.png create mode 100644 bitbake/lib/bb/ui/icons/indicators/denied.png create mode 100644 bitbake/lib/bb/ui/icons/indicators/error.png create mode 100644 bitbake/lib/bb/ui/icons/indicators/info.png create mode 100644 bitbake/lib/bb/ui/icons/indicators/issues.png create mode 100644 bitbake/lib/bb/ui/icons/indicators/refresh.png create mode 100644 bitbake/lib/bb/ui/icons/indicators/remove-hover.png create mode 100644 bitbake/lib/bb/ui/icons/indicators/remove.png create mode 100644 bitbake/lib/bb/ui/icons/indicators/tick.png create mode 100644 bitbake/lib/bb/ui/icons/info/info_display.png create mode 100644 bitbake/lib/bb/ui/icons/info/info_hover.png create mode 100644 bitbake/lib/bb/ui/icons/layers/layers_display.png create mode 100644 bitbake/lib/bb/ui/icons/layers/layers_hover.png create mode 100644 bitbake/lib/bb/ui/icons/packages/packages_display.png create mode 100644 bitbake/lib/bb/ui/icons/packages/packages_hover.png create mode 100644 bitbake/lib/bb/ui/icons/recipe/recipe_display.png create mode 100644 bitbake/lib/bb/ui/icons/recipe/recipe_hover.png create mode 100644 bitbake/lib/bb/ui/icons/settings/settings_display.png create mode 100644 bitbake/lib/bb/ui/icons/settings/settings_hover.png create mode 100644 bitbake/lib/bb/ui/icons/templates/templates_display.png create mode 100644 bitbake/lib/bb/ui/icons/templates/templates_hover.png create mode 100644 bitbake/lib/bb/ui/knotty.py create mode 100644 bitbake/lib/bb/ui/ncurses.py create mode 100644 bitbake/lib/bb/ui/puccho.py create mode 100644 bitbake/lib/bb/ui/toasterui.py create mode 100644 bitbake/lib/bb/ui/uievent.py create mode 100644 bitbake/lib/bb/ui/uihelper.py (limited to 'bitbake/lib/bb/ui') diff --git a/bitbake/lib/bb/ui/__init__.py b/bitbake/lib/bb/ui/__init__.py new file mode 100644 index 0000000000..a4805ed028 --- /dev/null +++ b/bitbake/lib/bb/ui/__init__.py @@ -0,0 +1,17 @@ +# +# BitBake UI Implementation +# +# Copyright (C) 2006-2007 Richard Purdie +# +# 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. diff --git a/bitbake/lib/bb/ui/buildinfohelper.py b/bitbake/lib/bb/ui/buildinfohelper.py new file mode 100644 index 0000000000..69655709d3 --- /dev/null +++ b/bitbake/lib/bb/ui/buildinfohelper.py @@ -0,0 +1,964 @@ +# +# BitBake ToasterUI Implementation +# +# Copyright (C) 2013 Intel Corporation +# +# 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 datetime +import sys +import bb +import re +import ast + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "toaster.toastermain.settings") + +import toaster.toastermain.settings as toaster_django_settings +from toaster.orm.models import Build, Task, Recipe, Layer_Version, Layer, Target, LogMessage, HelpText +from toaster.orm.models import Target_Image_File +from toaster.orm.models import Variable, VariableHistory +from toaster.orm.models import Package, Package_File, Target_Installed_Package, Target_File +from toaster.orm.models import Task_Dependency, Package_Dependency +from toaster.orm.models import Recipe_Dependency +from bb.msg import BBLogFormatter as format + +class NotExisting(Exception): + pass + +class ORMWrapper(object): + """ This class creates the dictionaries needed to store information in the database + following the format defined by the Django models. It is also used to save this + information in the database. + """ + + def __init__(self): + pass + + + def create_build_object(self, build_info): + assert 'machine' in build_info + assert 'distro' in build_info + assert 'distro_version' in build_info + assert 'started_on' in build_info + assert 'cooker_log_path' in build_info + assert 'build_name' in build_info + assert 'bitbake_version' in build_info + + build = Build.objects.create( + machine=build_info['machine'], + distro=build_info['distro'], + distro_version=build_info['distro_version'], + started_on=build_info['started_on'], + completed_on=build_info['started_on'], + cooker_log_path=build_info['cooker_log_path'], + build_name=build_info['build_name'], + bitbake_version=build_info['bitbake_version']) + + return build + + def create_target_objects(self, target_info): + assert 'build' in target_info + assert 'targets' in target_info + + targets = [] + for tgt_name in target_info['targets']: + tgt_object = Target.objects.create( build = target_info['build'], + target = tgt_name, + is_image = False, + ); + targets.append(tgt_object) + return targets + + def update_build_object(self, build, errors, warnings, taskfailures): + assert isinstance(build,Build) + assert isinstance(errors, int) + assert isinstance(warnings, int) + + outcome = Build.SUCCEEDED + if errors or taskfailures: + outcome = Build.FAILED + + build.completed_on = datetime.datetime.now() + build.timespent = int((build.completed_on - build.started_on).total_seconds()) + build.errors_no = errors + build.warnings_no = warnings + build.outcome = outcome + build.save() + + def update_target_object(self, target, license_manifest_path): + + target.license_manifest_path = license_manifest_path + target.save() + + def get_update_task_object(self, task_information, must_exist = False): + assert 'build' in task_information + assert 'recipe' in task_information + assert 'task_name' in task_information + + task_object, created = Task.objects.get_or_create( + build=task_information['build'], + recipe=task_information['recipe'], + task_name=task_information['task_name'], + ) + + if must_exist and created: + task_information['debug'] = "build id %d, recipe id %d" % (task_information['build'].pk, task_information['recipe'].pk) + task_object.delete() + raise NotExisting("Task object created when expected to exist", task_information) + + for v in vars(task_object): + if v in task_information.keys(): + vars(task_object)[v] = task_information[v] + + # update setscene-related information + if 1 == Task.objects.related_setscene(task_object).count(): + if task_object.outcome == Task.OUTCOME_COVERED: + task_object.outcome = Task.OUTCOME_CACHED + + outcome_task_setscene = Task.objects.get(task_executed=True, build = task_object.build, + recipe = task_object.recipe, task_name=task_object.task_name+"_setscene").outcome + if outcome_task_setscene == Task.OUTCOME_SUCCESS: + task_object.sstate_result = Task.SSTATE_RESTORED + elif outcome_task_setscene == Task.OUTCOME_FAILED: + task_object.sstate_result = Task.SSTATE_FAILED + + # mark down duration if we have a start time and a current time + if 'start_time' in task_information.keys() and 'end_time' in task_information.keys(): + duration = task_information['end_time'] - task_information['start_time'] + task_object.elapsed_time = duration + + task_object.save() + return task_object + + + def get_update_recipe_object(self, recipe_information, must_exist = False): + assert 'layer_version' in recipe_information + assert 'file_path' in recipe_information + + + recipe_object, created = Recipe.objects.get_or_create( + layer_version=recipe_information['layer_version'], + file_path=recipe_information['file_path']) + + if must_exist and created: + recipe_object.delete() + raise NotExisting("Recipe object created when expected to exist", recipe_information) + + for v in vars(recipe_object): + if v in recipe_information.keys(): + vars(recipe_object)[v] = recipe_information[v] + + recipe_object.save() + + return recipe_object + + def get_update_layer_version_object(self, build_obj, layer_obj, layer_version_information): + assert isinstance(build_obj, Build) + assert isinstance(layer_obj, Layer) + assert 'branch' in layer_version_information + assert 'commit' in layer_version_information + assert 'priority' in layer_version_information + + layer_version_object, created = Layer_Version.objects.get_or_create( + build = build_obj, + layer = layer_obj, + branch = layer_version_information['branch'], + commit = layer_version_information['commit'], + priority = layer_version_information['priority'] + ) + + return layer_version_object + + def get_update_layer_object(self, layer_information): + assert 'name' in layer_information + assert 'local_path' in layer_information + assert 'layer_index_url' in layer_information + + layer_object, created = Layer.objects.get_or_create( + name=layer_information['name'], + local_path=layer_information['local_path'], + layer_index_url=layer_information['layer_index_url']) + + return layer_object + + def save_target_file_information(self, build_obj, target_obj, filedata): + assert isinstance(build_obj, Build) + assert isinstance(target_obj, Target) + dirs = filedata['dirs'] + files = filedata['files'] + syms = filedata['syms'] + + # we insert directories, ordered by name depth + for d in sorted(dirs, key=lambda x:len(x[-1].split("/"))): + (user, group, size) = d[1:4] + permission = d[0][1:] + path = d[4].lstrip(".") + if len(path) == 0: + # we create the root directory as a special case + path = "/" + tf_obj = Target_File.objects.create( + target = target_obj, + path = path, + size = size, + inodetype = Target_File.ITYPE_DIRECTORY, + permission = permission, + owner = user, + group = group, + ) + tf_obj.directory = tf_obj + tf_obj.save() + continue + parent_path = "/".join(path.split("/")[:len(path.split("/")) - 1]) + if len(parent_path) == 0: + parent_path = "/" + parent_obj = Target_File.objects.get(target = target_obj, path = parent_path, inodetype = Target_File.ITYPE_DIRECTORY) + tf_obj = Target_File.objects.create( + target = target_obj, + path = path, + size = size, + inodetype = Target_File.ITYPE_DIRECTORY, + permission = permission, + owner = user, + group = group, + directory = parent_obj) + + + # we insert files + for d in files: + (user, group, size) = d[1:4] + permission = d[0][1:] + path = d[4].lstrip(".") + parent_path = "/".join(path.split("/")[:len(path.split("/")) - 1]) + inodetype = Target_File.ITYPE_REGULAR + if d[0].startswith('b'): + inodetype = Target_File.ITYPE_BLOCK + if d[0].startswith('c'): + inodetype = Target_File.ITYPE_CHARACTER + if d[0].startswith('p'): + inodetype = Target_File.ITYPE_FIFO + + tf_obj = Target_File.objects.create( + target = target_obj, + path = path, + size = size, + inodetype = inodetype, + permission = permission, + owner = user, + group = group) + parent_obj = Target_File.objects.get(target = target_obj, path = parent_path, inodetype = Target_File.ITYPE_DIRECTORY) + tf_obj.directory = parent_obj + tf_obj.save() + + # we insert symlinks + for d in syms: + (user, group, size) = d[1:4] + permission = d[0][1:] + path = d[4].lstrip(".") + filetarget_path = d[6] + + parent_path = "/".join(path.split("/")[:len(path.split("/")) - 1]) + if not filetarget_path.startswith("/"): + # we have a relative path, get a normalized absolute one + filetarget_path = parent_path + "/" + filetarget_path + fcp = filetarget_path.split("/") + fcpl = [] + for i in fcp: + if i == "..": + fcpl.pop() + else: + fcpl.append(i) + filetarget_path = "/".join(fcpl) + + try: + filetarget_obj = Target_File.objects.get(target = target_obj, path = filetarget_path) + except Exception as e: + # we might have an invalid link; no way to detect this. just set it to None + filetarget_obj = None + + parent_obj = Target_File.objects.get(target = target_obj, path = parent_path, inodetype = Target_File.ITYPE_DIRECTORY) + + tf_obj = Target_File.objects.create( + target = target_obj, + path = path, + size = size, + inodetype = Target_File.ITYPE_SYMLINK, + permission = permission, + owner = user, + group = group, + directory = parent_obj, + sym_target = filetarget_obj) + + + def save_target_package_information(self, build_obj, target_obj, packagedict, pkgpnmap, recipes): + assert isinstance(build_obj, Build) + assert isinstance(target_obj, Target) + + errormsg = "" + for p in packagedict: + searchname = p + if 'OPKGN' in pkgpnmap[p].keys(): + searchname = pkgpnmap[p]['OPKGN'] + + packagedict[p]['object'], created = Package.objects.get_or_create( build = build_obj, name = searchname ) + if created: + # package was not build in the current build, but + # fill in everything we can from the runtime-reverse package data + try: + packagedict[p]['object'].recipe = recipes[pkgpnmap[p]['PN']] + packagedict[p]['object'].version = pkgpnmap[p]['PV'] + packagedict[p]['object'].installed_name = p + packagedict[p]['object'].revision = pkgpnmap[p]['PR'] + packagedict[p]['object'].license = pkgpnmap[p]['LICENSE'] + packagedict[p]['object'].section = pkgpnmap[p]['SECTION'] + packagedict[p]['object'].summary = pkgpnmap[p]['SUMMARY'] + packagedict[p]['object'].description = pkgpnmap[p]['DESCRIPTION'] + packagedict[p]['object'].size = int(pkgpnmap[p]['PKGSIZE']) + + # no files recorded for this package, so save files info + for targetpath in pkgpnmap[p]['FILES_INFO']: + targetfilesize = pkgpnmap[p]['FILES_INFO'][targetpath] + Package_File.objects.create( package = packagedict[p]['object'], + path = targetpath, + size = targetfilesize) + except KeyError as e: + errormsg += " stpi: Key error, package %s key %s \n" % ( p, e ) + + # save disk installed size + packagedict[p]['object'].installed_size = packagedict[p]['size'] + packagedict[p]['object'].save() + + Target_Installed_Package.objects.create(target = target_obj, package = packagedict[p]['object']) + + for p in packagedict: + for (px,deptype) in packagedict[p]['depends']: + if deptype == 'depends': + tdeptype = Package_Dependency.TYPE_TRDEPENDS + elif deptype == 'recommends': + tdeptype = Package_Dependency.TYPE_TRECOMMENDS + + Package_Dependency.objects.create( package = packagedict[p]['object'], + depends_on = packagedict[px]['object'], + dep_type = tdeptype, + target = target_obj); + + if (len(errormsg) > 0): + raise Exception(errormsg) + + def save_target_image_file_information(self, target_obj, file_name, file_size): + target_image_file = Target_Image_File.objects.create( target = target_obj, + file_name = file_name, + file_size = file_size) + target_image_file.save() + + def create_logmessage(self, log_information): + assert 'build' in log_information + assert 'level' in log_information + assert 'message' in log_information + + log_object = LogMessage.objects.create( + build = log_information['build'], + level = log_information['level'], + message = log_information['message']) + + for v in vars(log_object): + if v in log_information.keys(): + vars(log_object)[v] = log_information[v] + + return log_object.save() + + + def save_build_package_information(self, build_obj, package_info, recipes): + assert isinstance(build_obj, Build) + + # create and save the object + pname = package_info['PKG'] + if 'OPKGN' in package_info.keys(): + pname = package_info['OPKGN'] + + bp_object, created = Package.objects.get_or_create( build = build_obj, + name = pname ) + + bp_object.installed_name = package_info['PKG'] + bp_object.recipe = recipes[package_info['PN']] + bp_object.version = package_info['PKGV'] + bp_object.revision = package_info['PKGR'] + bp_object.summary = package_info['SUMMARY'] + bp_object.description = package_info['DESCRIPTION'] + bp_object.size = int(package_info['PKGSIZE']) + bp_object.section = package_info['SECTION'] + bp_object.license = package_info['LICENSE'] + bp_object.save() + + # save any attached file information + for path in package_info['FILES_INFO']: + fo = Package_File.objects.create( package = bp_object, + path = path, + size = package_info['FILES_INFO'][path] ) + + def _po_byname(p): + pkg, created = Package.objects.get_or_create(build = build_obj, name = p) + if created: + pkg.size = -1 + pkg.save() + return pkg + + # save soft dependency information + if 'RDEPENDS' in package_info and package_info['RDEPENDS']: + for p in bb.utils.explode_deps(package_info['RDEPENDS']): + Package_Dependency.objects.get_or_create( package = bp_object, + depends_on = _po_byname(p), dep_type = Package_Dependency.TYPE_RDEPENDS) + if 'RPROVIDES' in package_info and package_info['RPROVIDES']: + for p in bb.utils.explode_deps(package_info['RPROVIDES']): + Package_Dependency.objects.get_or_create( package = bp_object, + depends_on = _po_byname(p), dep_type = Package_Dependency.TYPE_RPROVIDES) + if 'RRECOMMENDS' in package_info and package_info['RRECOMMENDS']: + for p in bb.utils.explode_deps(package_info['RRECOMMENDS']): + Package_Dependency.objects.get_or_create( package = bp_object, + depends_on = _po_byname(p), dep_type = Package_Dependency.TYPE_RRECOMMENDS) + if 'RSUGGESTS' in package_info and package_info['RSUGGESTS']: + for p in bb.utils.explode_deps(package_info['RSUGGESTS']): + Package_Dependency.objects.get_or_create( package = bp_object, + depends_on = _po_byname(p), dep_type = Package_Dependency.TYPE_RSUGGESTS) + if 'RREPLACES' in package_info and package_info['RREPLACES']: + for p in bb.utils.explode_deps(package_info['RREPLACES']): + Package_Dependency.objects.get_or_create( package = bp_object, + depends_on = _po_byname(p), dep_type = Package_Dependency.TYPE_RREPLACES) + if 'RCONFLICTS' in package_info and package_info['RCONFLICTS']: + for p in bb.utils.explode_deps(package_info['RCONFLICTS']): + Package_Dependency.objects.get_or_create( package = bp_object, + depends_on = _po_byname(p), dep_type = Package_Dependency.TYPE_RCONFLICTS) + + return bp_object + + def save_build_variables(self, build_obj, vardump): + assert isinstance(build_obj, Build) + + for k in vardump: + desc = vardump[k]['doc']; + if desc is None: + var_words = [word for word in k.split('_')] + root_var = "_".join([word for word in var_words if word.isupper()]) + if root_var and root_var != k and root_var in vardump: + desc = vardump[root_var]['doc'] + if desc is None: + desc = '' + if desc: + helptext_obj = HelpText.objects.create(build=build_obj, + area=HelpText.VARIABLE, + key=k, + text=desc) + if not bool(vardump[k]['func']): + value = vardump[k]['v']; + if value is None: + value = '' + variable_obj = Variable.objects.create( build = build_obj, + variable_name = k, + variable_value = value, + description = desc) + for vh in vardump[k]['history']: + if not 'documentation.conf' in vh['file']: + VariableHistory.objects.create( variable = variable_obj, + file_name = vh['file'], + line_number = vh['line'], + operation = vh['op']) + +class BuildInfoHelper(object): + """ This class gathers the build information from the server and sends it + towards the ORM wrapper for storing in the database + It is instantiated once per build + Keeps in memory all data that needs matching before writing it to the database + """ + + def __init__(self, server, has_build_history = False): + self._configure_django() + self.internal_state = {} + self.internal_state['taskdata'] = {} + self.task_order = 0 + self.server = server + self.orm_wrapper = ORMWrapper() + self.has_build_history = has_build_history + self.tmp_dir = self.server.runCommand(["getVariable", "TMPDIR"])[0] + + def _configure_django(self): + # Add toaster to sys path for importing modules + sys.path.append(os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), 'toaster')) + + ################### + ## methods to convert event/external info into objects that the ORM layer uses + + + def _get_build_information(self): + build_info = {} + # Generate an identifier for each new build + + build_info['machine'] = self.server.runCommand(["getVariable", "MACHINE"])[0] + build_info['distro'] = self.server.runCommand(["getVariable", "DISTRO"])[0] + build_info['distro_version'] = self.server.runCommand(["getVariable", "DISTRO_VERSION"])[0] + build_info['started_on'] = datetime.datetime.now() + build_info['completed_on'] = datetime.datetime.now() + build_info['cooker_log_path'] = self.server.runCommand(["getVariable", "BB_CONSOLELOG"])[0] + build_info['build_name'] = self.server.runCommand(["getVariable", "BUILDNAME"])[0] + build_info['bitbake_version'] = self.server.runCommand(["getVariable", "BB_VERSION"])[0] + + return build_info + + def _get_task_information(self, event, recipe): + assert 'taskname' in vars(event) + + task_information = {} + task_information['build'] = self.internal_state['build'] + task_information['outcome'] = Task.OUTCOME_NA + task_information['recipe'] = recipe + task_information['task_name'] = event.taskname + try: + # some tasks don't come with a hash. and that's ok + task_information['sstate_checksum'] = event.taskhash + except AttributeError: + pass + return task_information + + def _get_layer_version_for_path(self, path): + assert path.startswith("/") + assert 'build' in self.internal_state + + def _slkey(layer_version): + assert isinstance(layer_version, Layer_Version) + return len(layer_version.layer.local_path) + + # Heuristics: we always match recipe to the deepest layer path that + # we can match to the recipe file path + for bl in sorted(Layer_Version.objects.filter(build = self.internal_state['build']), reverse=True, key=_slkey): + if (path.startswith(bl.layer.local_path)): + return bl + + #TODO: if we get here, we didn't read layers correctly + assert False + return None + + def _get_recipe_information_from_taskfile(self, taskfile): + localfilepath = taskfile.split(":")[-1] + layer_version_obj = self._get_layer_version_for_path(localfilepath) + + recipe_info = {} + recipe_info['layer_version'] = layer_version_obj + recipe_info['file_path'] = taskfile + + return recipe_info + + def _get_path_information(self, task_object): + assert isinstance(task_object, Task) + build_stats_format = "{tmpdir}/buildstats/{target}-{machine}/{buildname}/{package}/" + build_stats_path = [] + + for t in self.internal_state['targets']: + target = t.target + machine = self.internal_state['build'].machine + buildname = self.internal_state['build'].build_name + pe, pv = task_object.recipe.version.split(":",1) + if len(pe) > 0: + package = task_object.recipe.name + "-" + pe + "_" + pv + else: + package = task_object.recipe.name + "-" + pv + + build_stats_path.append(build_stats_format.format(tmpdir=self.tmp_dir, target=target, + machine=machine, buildname=buildname, + package=package)) + + return build_stats_path + + def _remove_redundant(self, string): + ret = [] + for i in string.split(): + if i not in ret: + ret.append(i) + return " ".join(sorted(ret)) + + + ################################ + ## external available methods to store information + + def store_layer_info(self, event): + assert 'data' in vars(event) + layerinfos = event.data + self.internal_state['lvs'] = {} + for layer in layerinfos: + self.internal_state['lvs'][self.orm_wrapper.get_update_layer_object(layerinfos[layer])] = layerinfos[layer]['version'] + + + def store_started_build(self, event): + assert '_pkgs' in vars(event) + build_information = self._get_build_information() + + build_obj = self.orm_wrapper.create_build_object(build_information) + self.internal_state['build'] = build_obj + + # save layer version information for this build + for layer_obj in self.internal_state['lvs']: + self.orm_wrapper.get_update_layer_version_object(build_obj, layer_obj, self.internal_state['lvs'][layer_obj]) + + del self.internal_state['lvs'] + + # create target information + target_information = {} + target_information['targets'] = event._pkgs + target_information['build'] = build_obj + + self.internal_state['targets'] = self.orm_wrapper.create_target_objects(target_information) + + # Save build configuration + self.orm_wrapper.save_build_variables(build_obj, self.server.runCommand(["getAllKeysWithFlags", ["doc", "func"]])[0]) + + def update_target_image_file(self, event): + image_fstypes = self.server.runCommand(["getVariable", "IMAGE_FSTYPES"])[0] + for t in self.internal_state['targets']: + if t.is_image == True: + output_files = list(event.data.viewkeys()) + for output in output_files: + if t.target in output and output.split('.rootfs.')[1] in image_fstypes: + self.orm_wrapper.save_target_image_file_information(t, output, event.data[output]) + + def update_build_information(self, event, errors, warnings, taskfailures): + if 'build' in self.internal_state: + self.orm_wrapper.update_build_object(self.internal_state['build'], errors, warnings, taskfailures) + + + def store_license_manifest_path(self, event): + deploy_dir = event.data['deploy_dir'] + image_name = event.data['image_name'] + path = deploy_dir + "/licenses/" + image_name + "/" + for target in self.internal_state['targets']: + if target.target in image_name: + self.orm_wrapper.update_target_object(target, path) + + + def store_started_task(self, event): + assert isinstance(event, (bb.runqueue.sceneQueueTaskStarted, bb.runqueue.runQueueTaskStarted, bb.runqueue.runQueueTaskSkipped)) + assert 'taskfile' in vars(event) + localfilepath = event.taskfile.split(":")[-1] + assert localfilepath.startswith("/") + + identifier = event.taskfile + ":" + event.taskname + + recipe_information = self._get_recipe_information_from_taskfile(event.taskfile) + recipe = self.orm_wrapper.get_update_recipe_object(recipe_information, True) + + task_information = self._get_task_information(event, recipe) + task_information['outcome'] = Task.OUTCOME_NA + + if isinstance(event, bb.runqueue.runQueueTaskSkipped): + assert 'reason' in vars(event) + task_information['task_executed'] = False + if event.reason == "covered": + task_information['outcome'] = Task.OUTCOME_COVERED + if event.reason == "existing": + task_information['outcome'] = Task.OUTCOME_PREBUILT + else: + task_information['task_executed'] = True + if 'noexec' in vars(event) and event.noexec == True: + task_information['task_executed'] = False + task_information['outcome'] = Task.OUTCOME_EMPTY + task_information['script_type'] = Task.CODING_NA + + # do not assign order numbers to scene tasks + if not isinstance(event, bb.runqueue.sceneQueueTaskStarted): + self.task_order += 1 + task_information['order'] = self.task_order + + task_obj = self.orm_wrapper.get_update_task_object(task_information) + + self.internal_state['taskdata'][identifier] = { + 'outcome': task_information['outcome'], + } + + + def store_tasks_stats(self, event): + for (taskfile, taskname, taskstats, recipename) in event.data: + localfilepath = taskfile.split(":")[-1] + assert localfilepath.startswith("/") + + recipe_information = self._get_recipe_information_from_taskfile(taskfile) + recipe_object = Recipe.objects.get(layer_version = recipe_information['layer_version'], + file_path__endswith = recipe_information['file_path'], + name = recipename) + + task_information = {} + task_information['build'] = self.internal_state['build'] + task_information['recipe'] = recipe_object + task_information['task_name'] = taskname + task_information['cpu_usage'] = taskstats['cpu_usage'] + task_information['disk_io'] = taskstats['disk_io'] + task_obj = self.orm_wrapper.get_update_task_object(task_information, True) # must exist + + def update_and_store_task(self, event): + assert 'taskfile' in vars(event) + localfilepath = event.taskfile.split(":")[-1] + assert localfilepath.startswith("/") + + identifier = event.taskfile + ":" + event.taskname + if not identifier in self.internal_state['taskdata']: + if isinstance(event, bb.build.TaskBase): + # we do a bit of guessing + candidates = [x for x in self.internal_state['taskdata'].keys() if x.endswith(identifier)] + if len(candidates) == 1: + identifier = candidates[0] + + assert identifier in self.internal_state['taskdata'] + identifierlist = identifier.split(":") + realtaskfile = ":".join(identifierlist[0:len(identifierlist)-1]) + recipe_information = self._get_recipe_information_from_taskfile(realtaskfile) + recipe = self.orm_wrapper.get_update_recipe_object(recipe_information, True) + task_information = self._get_task_information(event,recipe) + + if 'time' in vars(event): + if not 'start_time' in self.internal_state['taskdata'][identifier]: + self.internal_state['taskdata'][identifier]['start_time'] = event.time + else: + task_information['end_time'] = event.time + task_information['start_time'] = self.internal_state['taskdata'][identifier]['start_time'] + + task_information['outcome'] = self.internal_state['taskdata'][identifier]['outcome'] + + if 'logfile' in vars(event): + task_information['logfile'] = event.logfile + + if '_message' in vars(event): + task_information['message'] = event._message + + if 'taskflags' in vars(event): + # with TaskStarted, we get even more information + if 'python' in event.taskflags.keys() and event.taskflags['python'] == '1': + task_information['script_type'] = Task.CODING_PYTHON + else: + task_information['script_type'] = Task.CODING_SHELL + + if task_information['outcome'] == Task.OUTCOME_NA: + if isinstance(event, (bb.runqueue.runQueueTaskCompleted, bb.runqueue.sceneQueueTaskCompleted)): + task_information['outcome'] = Task.OUTCOME_SUCCESS + del self.internal_state['taskdata'][identifier] + + if isinstance(event, (bb.runqueue.runQueueTaskFailed, bb.runqueue.sceneQueueTaskFailed)): + task_information['outcome'] = Task.OUTCOME_FAILED + del self.internal_state['taskdata'][identifier] + + self.orm_wrapper.get_update_task_object(task_information, True) # must exist + + + def store_missed_state_tasks(self, event): + for (fn, taskname, taskhash, sstatefile) in event.data['missed']: + + identifier = fn + taskname + "_setscene" + recipe_information = self._get_recipe_information_from_taskfile(fn) + recipe = self.orm_wrapper.get_update_recipe_object(recipe_information) + class MockEvent: pass + mevent = MockEvent() + mevent.taskname = taskname + mevent.taskhash = taskhash + task_information = self._get_task_information(mevent,recipe) + + task_information['start_time'] = datetime.datetime.now() + task_information['outcome'] = Task.OUTCOME_NA + task_information['sstate_checksum'] = taskhash + task_information['sstate_result'] = Task.SSTATE_MISS + task_information['path_to_sstate_obj'] = sstatefile + + self.orm_wrapper.get_update_task_object(task_information) + + for (fn, taskname, taskhash, sstatefile) in event.data['found']: + + identifier = fn + taskname + "_setscene" + recipe_information = self._get_recipe_information_from_taskfile(fn) + recipe = self.orm_wrapper.get_update_recipe_object(recipe_information) + class MockEvent: pass + mevent = MockEvent() + mevent.taskname = taskname + mevent.taskhash = taskhash + task_information = self._get_task_information(mevent,recipe) + + task_information['path_to_sstate_obj'] = sstatefile + + self.orm_wrapper.get_update_task_object(task_information) + + + def store_target_package_data(self, event): + assert 'data' in vars(event) + # for all image targets + for target in self.internal_state['targets']: + if target.is_image: + try: + pkgdata = event.data['pkgdata'] + imgdata = event.data['imgdata'][target.target] + self.orm_wrapper.save_target_package_information(self.internal_state['build'], target, imgdata, pkgdata, self.internal_state['recipes']) + filedata = event.data['filedata'][target.target] + self.orm_wrapper.save_target_file_information(self.internal_state['build'], target, filedata) + except KeyError: + # we must have not got the data for this image, nothing to save + pass + + + + def store_dependency_information(self, event): + assert '_depgraph' in vars(event) + assert 'layer-priorities' in event._depgraph + assert 'pn' in event._depgraph + assert 'tdepends' in event._depgraph + + errormsg = "" + + # save layer version priorities + if 'layer-priorities' in event._depgraph.keys(): + for lv in event._depgraph['layer-priorities']: + (name, path, regexp, priority) = lv + layer_version_obj = self._get_layer_version_for_path(path[1:]) # paths start with a ^ + assert layer_version_obj is not None + layer_version_obj.priority = priority + layer_version_obj.save() + + # save recipe information + self.internal_state['recipes'] = {} + for pn in event._depgraph['pn']: + + file_name = event._depgraph['pn'][pn]['filename'] + layer_version_obj = self._get_layer_version_for_path(file_name.split(":")[-1]) + + assert layer_version_obj is not None + + recipe_info = {} + recipe_info['name'] = pn + recipe_info['version'] = event._depgraph['pn'][pn]['version'].lstrip(":") + recipe_info['layer_version'] = layer_version_obj + recipe_info['summary'] = event._depgraph['pn'][pn]['summary'] + recipe_info['license'] = event._depgraph['pn'][pn]['license'] + recipe_info['description'] = event._depgraph['pn'][pn]['description'] + recipe_info['section'] = event._depgraph['pn'][pn]['section'] + recipe_info['homepage'] = event._depgraph['pn'][pn]['homepage'] + recipe_info['bugtracker'] = event._depgraph['pn'][pn]['bugtracker'] + recipe_info['file_path'] = file_name + recipe = self.orm_wrapper.get_update_recipe_object(recipe_info) + recipe.is_image = False + if 'inherits' in event._depgraph['pn'][pn].keys(): + for cls in event._depgraph['pn'][pn]['inherits']: + if cls.endswith('/image.bbclass'): + recipe.is_image = True + break + if recipe.is_image: + for t in self.internal_state['targets']: + if pn == t.target: + t.is_image = True + t.save() + self.internal_state['recipes'][pn] = recipe + + # we'll not get recipes for key w/ values listed in ASSUME_PROVIDED + + assume_provided = self.server.runCommand(["getVariable", "ASSUME_PROVIDED"])[0].split() + + # save recipe dependency + # buildtime + for recipe in event._depgraph['depends']: + try: + target = self.internal_state['recipes'][recipe] + for dep in event._depgraph['depends'][recipe]: + dependency = self.internal_state['recipes'][dep] + Recipe_Dependency.objects.get_or_create( recipe = target, + depends_on = dependency, dep_type = Recipe_Dependency.TYPE_DEPENDS) + except KeyError as e: + if e not in assume_provided and not str(e).startswith("virtual/"): + errormsg += " stpd: KeyError saving recipe dependency for %s, %s \n" % (recipe, e) + + # save all task information + def _save_a_task(taskdesc): + spec = re.split(r'\.', taskdesc); + pn = ".".join(spec[0:-1]) + taskname = spec[-1] + e = event + e.taskname = pn + recipe = self.internal_state['recipes'][pn] + task_info = self._get_task_information(e, recipe) + task_info['task_name'] = taskname + task_obj = self.orm_wrapper.get_update_task_object(task_info) + return task_obj + + # create tasks + tasks = {} + for taskdesc in event._depgraph['tdepends']: + tasks[taskdesc] = _save_a_task(taskdesc) + + # create dependencies between tasks + for taskdesc in event._depgraph['tdepends']: + target = tasks[taskdesc] + for taskdep in event._depgraph['tdepends'][taskdesc]: + if taskdep not in tasks: + # Fetch tasks info is not collected previously + dep = _save_a_task(taskdep) + else: + dep = tasks[taskdep] + Task_Dependency.objects.get_or_create( task = target, depends_on = dep ) + + if (len(errormsg) > 0): + raise Exception(errormsg) + + + def store_build_package_information(self, event): + assert 'data' in vars(event) + package_info = event.data + self.orm_wrapper.save_build_package_information(self.internal_state['build'], + package_info, + self.internal_state['recipes'], + ) + + def _store_log_information(self, level, text): + log_information = {} + log_information['build'] = self.internal_state['build'] + log_information['level'] = level + log_information['message'] = text + self.orm_wrapper.create_logmessage(log_information) + + def store_log_info(self, text): + self._store_log_information(LogMessage.INFO, text) + + def store_log_warn(self, text): + self._store_log_information(LogMessage.WARNING, text) + + def store_log_error(self, text): + self._store_log_information(LogMessage.ERROR, text) + + def store_log_event(self, event): + if 'build' in self.internal_state and 'backlog' in self.internal_state: + if len(self.internal_state['backlog']): + tempevent = self.internal_state['backlog'].pop() + print "Saving stored event ", tempevent + self.store_log_event(tempevent) + else: + del self.internal_state['backlog'] + + if event.levelno < format.WARNING: + return + + if not 'build' in self.internal_state: + print "Save event for later" + if not 'backlog' in self.internal_state: + self.internal_state['backlog'] = [] + self.internal_state['backlog'].append(event) + + return + log_information = {} + log_information['build'] = self.internal_state['build'] + if event.levelno >= format.ERROR: + log_information['level'] = LogMessage.ERROR + elif event.levelno == format.WARNING: + log_information['level'] = LogMessage.WARNING + log_information['message'] = event.msg + log_information['pathname'] = event.pathname + log_information['lineno'] = event.lineno + self.orm_wrapper.create_logmessage(log_information) + diff --git a/bitbake/lib/bb/ui/crumbs/__init__.py b/bitbake/lib/bb/ui/crumbs/__init__.py new file mode 100644 index 0000000000..b7cbe1a4f3 --- /dev/null +++ b/bitbake/lib/bb/ui/crumbs/__init__.py @@ -0,0 +1,17 @@ +# +# Gtk+ UI pieces for BitBake +# +# Copyright (C) 2006-2007 Richard Purdie +# +# 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. diff --git a/bitbake/lib/bb/ui/crumbs/builddetailspage.py b/bitbake/lib/bb/ui/crumbs/builddetailspage.py new file mode 100755 index 0000000000..7fc690e2fa --- /dev/null +++ b/bitbake/lib/bb/ui/crumbs/builddetailspage.py @@ -0,0 +1,437 @@ +#!/usr/bin/env python +# +# BitBake Graphical GTK User Interface +# +# Copyright (C) 2012 Intel Corporation +# +# Authored by Dongxiao Xu +# Authored by Shane Wang +# +# 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 pango +import gobject +import bb.process +from bb.ui.crumbs.progressbar import HobProgressBar +from bb.ui.crumbs.hobwidget import hic, HobNotebook, HobAltButton, HobWarpCellRendererText, HobButton, HobInfoButton +from bb.ui.crumbs.runningbuild import RunningBuildTreeView +from bb.ui.crumbs.runningbuild import BuildFailureTreeView +from bb.ui.crumbs.hobpages import HobPage +from bb.ui.crumbs.hobcolor import HobColors + +class BuildConfigurationTreeView(gtk.TreeView): + def __init__ (self): + gtk.TreeView.__init__(self) + self.set_rules_hint(False) + self.set_headers_visible(False) + self.set_property("hover-expand", True) + self.get_selection().set_mode(gtk.SELECTION_SINGLE) + + # The icon that indicates whether we're building or failed. + renderer0 = gtk.CellRendererText() + renderer0.set_property('font-desc', pango.FontDescription('courier bold 12')) + col0 = gtk.TreeViewColumn ("Name", renderer0, text=0) + self.append_column (col0) + + # The message of configuration. + renderer1 = HobWarpCellRendererText(col_number=1) + col1 = gtk.TreeViewColumn ("Values", renderer1, text=1) + self.append_column (col1) + + def set_vars(self, key="", var=[""]): + d = {} + if type(var) == str: + d = {key: [var]} + elif type(var) == list and len(var) > 1: + #create the sub item line + l = [] + text = "" + for item in var: + text = " - " + item + l.append(text) + d = {key: var} + + return d + + def set_config_model(self, show_vars): + listmodel = gtk.TreeStore(gobject.TYPE_STRING, gobject.TYPE_STRING) + parent = None + for var in show_vars: + for subitem in var.items(): + name = subitem[0] + is_parent = True + for value in subitem[1]: + if is_parent: + parent = listmodel.append(parent, (name, value)) + is_parent = False + else: + listmodel.append(parent, (None, value)) + name = " - " + parent = None + # renew the tree model after get the configuration messages + self.set_model(listmodel) + + def show(self, src_config_info, src_params): + vars = [] + vars.append(self.set_vars("BB version:", src_params.bb_version)) + vars.append(self.set_vars("Target arch:", src_params.target_arch)) + vars.append(self.set_vars("Target OS:", src_params.target_os)) + vars.append(self.set_vars("Machine:", src_config_info.curr_mach)) + vars.append(self.set_vars("Distro:", src_config_info.curr_distro)) + vars.append(self.set_vars("Distro version:", src_params.distro_version)) + vars.append(self.set_vars("SDK machine:", src_config_info.curr_sdk_machine)) + vars.append(self.set_vars("Tune features:", src_params.tune_pkgarch)) + vars.append(self.set_vars("Layers:", src_config_info.layers)) + + for path in src_config_info.layers: + import os, os.path + if os.path.exists(path): + branch = bb.process.run('cd %s; git branch | grep "^* " | tr -d "* "' % path)[0] + if branch.startswith("fatal:"): + branch = "(unknown)" + if branch: + branch = branch.strip('\n') + vars.append(self.set_vars("Branch:", branch)) + break + + self.set_config_model(vars) + + def reset(self): + self.set_model(None) + +# +# BuildDetailsPage +# + +class BuildDetailsPage (HobPage): + + def __init__(self, builder): + super(BuildDetailsPage, self).__init__(builder, "Building ...") + + self.num_of_issues = 0 + self.endpath = (0,) + # create visual elements + self.create_visual_elements() + + def create_visual_elements(self): + # create visual elements + self.vbox = gtk.VBox(False, 12) + + self.progress_box = gtk.VBox(False, 12) + self.task_status = gtk.Label("\n") # to ensure layout is correct + self.task_status.set_alignment(0.0, 0.5) + self.progress_box.pack_start(self.task_status, expand=False, fill=False) + self.progress_hbox = gtk.HBox(False, 6) + self.progress_box.pack_end(self.progress_hbox, expand=True, fill=True) + self.progress_bar = HobProgressBar() + self.progress_hbox.pack_start(self.progress_bar, expand=True, fill=True) + self.stop_button = HobAltButton("Stop") + self.stop_button.connect("clicked", self.stop_button_clicked_cb) + self.stop_button.set_sensitive(False) + self.progress_hbox.pack_end(self.stop_button, expand=False, fill=False) + + self.notebook = HobNotebook() + self.config_tv = BuildConfigurationTreeView() + self.scrolled_view_config = gtk.ScrolledWindow () + self.scrolled_view_config.set_policy(gtk.POLICY_NEVER, gtk.POLICY_ALWAYS) + self.scrolled_view_config.add(self.config_tv) + self.notebook.append_page(self.scrolled_view_config, "Build configuration") + + self.failure_tv = BuildFailureTreeView() + self.failure_model = self.builder.handler.build.model.failure_model() + self.failure_tv.set_model(self.failure_model) + self.scrolled_view_failure = gtk.ScrolledWindow () + self.scrolled_view_failure.set_policy(gtk.POLICY_NEVER, gtk.POLICY_ALWAYS) + self.scrolled_view_failure.add(self.failure_tv) + self.notebook.append_page(self.scrolled_view_failure, "Issues") + + self.build_tv = RunningBuildTreeView(readonly=True, hob=True) + self.build_tv.set_model(self.builder.handler.build.model) + self.scrolled_view_build = gtk.ScrolledWindow () + self.scrolled_view_build.set_policy(gtk.POLICY_NEVER, gtk.POLICY_ALWAYS) + self.scrolled_view_build.add(self.build_tv) + self.notebook.append_page(self.scrolled_view_build, "Log") + + self.builder.handler.build.model.connect_after("row-changed", self.scroll_to_present_row, self.scrolled_view_build.get_vadjustment(), self.build_tv) + + self.button_box = gtk.HBox(False, 6) + self.back_button = HobAltButton('<< Back') + self.back_button.connect("clicked", self.back_button_clicked_cb) + self.button_box.pack_start(self.back_button, expand=False, fill=False) + + def update_build_status(self, current, total, task): + recipe_path, recipe_task = task.split(", ") + recipe = os.path.basename(recipe_path).rstrip(".bb") + tsk_msg = "Running task %s of %s: %s\nRecipe: %s" % (current, total, recipe_task, recipe) + self.task_status.set_markup(tsk_msg) + self.stop_button.set_sensitive(True) + + def reset_build_status(self): + self.task_status.set_markup("\n") # to ensure layout is correct + self.endpath = (0,) + + def show_issues(self): + self.num_of_issues += 1 + self.notebook.show_indicator_icon("Issues", self.num_of_issues) + self.notebook.queue_draw() + + def reset_issues(self): + self.num_of_issues = 0 + self.notebook.hide_indicator_icon("Issues") + + def _remove_all_widget(self): + children = self.vbox.get_children() or [] + for child in children: + self.vbox.remove(child) + children = self.box_group_area.get_children() or [] + for child in children: + self.box_group_area.remove(child) + children = self.get_children() or [] + for child in children: + self.remove(child) + + def add_build_fail_top_bar(self, actions, log_file=None): + primary_action = "Edit %s" % actions + + color = HobColors.ERROR + build_fail_top = gtk.EventBox() + #build_fail_top.set_size_request(-1, 200) + build_fail_top.modify_bg(gtk.STATE_NORMAL, gtk.gdk.color_parse(color)) + + build_fail_tab = gtk.Table(14, 46, True) + build_fail_top.add(build_fail_tab) + + icon = gtk.Image() + icon_pix_buffer = gtk.gdk.pixbuf_new_from_file(hic.ICON_INDI_ERROR_FILE) + icon.set_from_pixbuf(icon_pix_buffer) + build_fail_tab.attach(icon, 1, 4, 0, 6) + + label = gtk.Label() + label.set_alignment(0.0, 0.5) + label.set_markup("%s" % self.title) + build_fail_tab.attach(label, 4, 26, 0, 6) + + label = gtk.Label() + label.set_alignment(0.0, 0.5) + # Ensure variable disk_full is defined + if not hasattr(self.builder, 'disk_full'): + self.builder.disk_full = False + + if self.builder.disk_full: + markup = "There is no disk space left, so Hob cannot finish building your image. Free up some disk space\n" + markup += "and restart the build. Check the \"Issues\" tab for more details" + label.set_markup(markup) + else: + label.set_markup("Check the \"Issues\" information for more details") + build_fail_tab.attach(label, 4, 40, 4, 9) + + # create button 'Edit packages' + action_button = HobButton(primary_action) + #action_button.set_size_request(-1, 40) + action_button.set_tooltip_text("Edit the %s parameters" % actions) + action_button.connect('clicked', self.failure_primary_action_button_clicked_cb, primary_action) + + if log_file: + open_log_button = HobAltButton("Open log") + open_log_button.set_relief(gtk.RELIEF_HALF) + open_log_button.set_tooltip_text("Open the build's log file") + open_log_button.connect('clicked', self.open_log_button_clicked_cb, log_file) + + attach_pos = (24 if log_file else 14) + file_bug_button = HobAltButton('File a bug') + file_bug_button.set_relief(gtk.RELIEF_HALF) + file_bug_button.set_tooltip_text("Open the Yocto Project bug tracking website") + file_bug_button.connect('clicked', self.failure_activate_file_bug_link_cb) + + if not self.builder.disk_full: + build_fail_tab.attach(action_button, 4, 13, 9, 12) + if log_file: + build_fail_tab.attach(open_log_button, 14, 23, 9, 12) + build_fail_tab.attach(file_bug_button, attach_pos, attach_pos + 9, 9, 12) + + else: + restart_build = HobButton("Restart the build") + restart_build.set_tooltip_text("Restart the build") + restart_build.connect('clicked', self.restart_build_button_clicked_cb) + + build_fail_tab.attach(restart_build, 4, 13, 9, 12) + build_fail_tab.attach(action_button, 14, 23, 9, 12) + if log_file: + build_fail_tab.attach(open_log_button, attach_pos, attach_pos + 9, 9, 12) + + self.builder.disk_full = False + return build_fail_top + + def show_fail_page(self, title): + self._remove_all_widget() + self.title = "Hob cannot build your %s" % title + + self.build_fail_bar = self.add_build_fail_top_bar(title, self.builder.current_logfile) + + self.pack_start(self.group_align, expand=True, fill=True) + self.box_group_area.pack_start(self.build_fail_bar, expand=False, fill=False) + self.box_group_area.pack_start(self.vbox, expand=True, fill=True) + + self.vbox.pack_start(self.notebook, expand=True, fill=True) + self.show_all() + self.notebook.set_page("Issues") + self.back_button.hide() + + def add_build_stop_top_bar(self, action, log_file=None): + color = HobColors.LIGHT_GRAY + build_stop_top = gtk.EventBox() + #build_stop_top.set_size_request(-1, 200) + build_stop_top.modify_bg(gtk.STATE_NORMAL, gtk.gdk.color_parse(color)) + build_stop_top.set_flags(gtk.CAN_DEFAULT) + build_stop_top.grab_default() + + build_stop_tab = gtk.Table(11, 46, True) + build_stop_top.add(build_stop_tab) + + icon = gtk.Image() + icon_pix_buffer = gtk.gdk.pixbuf_new_from_file(hic.ICON_INFO_HOVER_FILE) + icon.set_from_pixbuf(icon_pix_buffer) + build_stop_tab.attach(icon, 1, 4, 0, 6) + + label = gtk.Label() + label.set_alignment(0.0, 0.5) + label.set_markup("%s" % self.title) + build_stop_tab.attach(label, 4, 26, 0, 6) + + action_button = HobButton("Edit %s" % action) + action_button.set_size_request(-1, 40) + if action == "image": + action_button.set_tooltip_text("Edit the image parameters") + elif action == "recipes": + action_button.set_tooltip_text("Edit the included recipes") + elif action == "packages": + action_button.set_tooltip_text("Edit the included packages") + action_button.connect('clicked', self.stop_primary_action_button_clicked_cb, action) + build_stop_tab.attach(action_button, 4, 13, 6, 9) + + if log_file: + open_log_button = HobAltButton("Open log") + open_log_button.set_relief(gtk.RELIEF_HALF) + open_log_button.set_tooltip_text("Open the build's log file") + open_log_button.connect('clicked', self.open_log_button_clicked_cb, log_file) + build_stop_tab.attach(open_log_button, 14, 23, 6, 9) + + attach_pos = (24 if log_file else 14) + build_button = HobAltButton("Build new image") + #build_button.set_size_request(-1, 40) + build_button.set_tooltip_text("Create a new image from scratch") + build_button.connect('clicked', self.new_image_button_clicked_cb) + build_stop_tab.attach(build_button, attach_pos, attach_pos + 9, 6, 9) + + return build_stop_top, action_button + + def show_stop_page(self, action): + self._remove_all_widget() + self.title = "Build stopped" + self.build_stop_bar, action_button = self.add_build_stop_top_bar(action, self.builder.current_logfile) + + self.pack_start(self.group_align, expand=True, fill=True) + self.box_group_area.pack_start(self.build_stop_bar, expand=False, fill=False) + self.box_group_area.pack_start(self.vbox, expand=True, fill=True) + + self.vbox.pack_start(self.notebook, expand=True, fill=True) + self.show_all() + self.back_button.hide() + return action_button + + def show_page(self, step): + self._remove_all_widget() + if step == self.builder.PACKAGE_GENERATING or step == self.builder.FAST_IMAGE_GENERATING: + self.title = "Building packages ..." + else: + self.title = "Building image ..." + self.build_details_top = self.add_onto_top_bar(None) + self.pack_start(self.build_details_top, expand=False, fill=False) + self.pack_start(self.group_align, expand=True, fill=True) + + self.box_group_area.pack_start(self.vbox, expand=True, fill=True) + + self.progress_bar.reset() + self.config_tv.reset() + self.vbox.pack_start(self.progress_box, expand=False, fill=False) + + self.vbox.pack_start(self.notebook, expand=True, fill=True) + + self.box_group_area.pack_end(self.button_box, expand=False, fill=False) + self.show_all() + self.notebook.set_page("Log") + self.back_button.hide() + + self.reset_build_status() + self.reset_issues() + + def update_progress_bar(self, title, fraction, status=None): + self.progress_bar.update(fraction) + self.progress_bar.set_title(title) + self.progress_bar.set_rcstyle(status) + + def back_button_clicked_cb(self, button): + self.builder.show_configuration() + + def new_image_button_clicked_cb(self, button): + self.builder.reset() + + def show_back_button(self): + self.back_button.show() + + def stop_button_clicked_cb(self, button): + self.builder.stop_build() + + def hide_stop_button(self): + self.stop_button.set_sensitive(False) + self.stop_button.hide() + + def scroll_to_present_row(self, model, path, iter, v_adj, treeview): + if treeview and v_adj: + if path[0] > self.endpath[0]: # check the event is a new row append or not + self.endpath = path + # check the gtk.adjustment position is at end boundary or not + if (v_adj.upper <= v_adj.page_size) or (v_adj.value == v_adj.upper - v_adj.page_size): + treeview.scroll_to_cell(path) + + def show_configurations(self, configurations, params): + self.config_tv.show(configurations, params) + + def failure_primary_action_button_clicked_cb(self, button, action): + if "Edit recipes" in action: + self.builder.show_recipes() + elif "Edit packages" in action: + self.builder.show_packages() + elif "Edit image" in action: + self.builder.show_configuration() + + def restart_build_button_clicked_cb(self, button): + self.builder.just_bake() + + def stop_primary_action_button_clicked_cb(self, button, action): + if "recipes" in action: + self.builder.show_recipes() + elif "packages" in action: + self.builder.show_packages() + elif "image" in action: + self.builder.show_configuration() + + def open_log_button_clicked_cb(self, button, log_file): + if log_file: + log_file = "file:///" + log_file + gtk.show_uri(screen=button.get_screen(), uri=log_file, timestamp=0) + + def failure_activate_file_bug_link_cb(self, button): + button.child.emit('activate-link', "http://bugzilla.yoctoproject.org") diff --git a/bitbake/lib/bb/ui/crumbs/builder.py b/bitbake/lib/bb/ui/crumbs/builder.py new file mode 100755 index 0000000000..455af320e8 --- /dev/null +++ b/bitbake/lib/bb/ui/crumbs/builder.py @@ -0,0 +1,1475 @@ +#!/usr/bin/env python +# +# BitBake Graphical GTK User Interface +# +# Copyright (C) 2011-2012 Intel Corporation +# +# Authored by Joshua Lock +# Authored by Dongxiao Xu +# Authored by Shane Wang +# +# 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 glib +import gtk, gobject +import copy +import os +import subprocess +import shlex +import re +import logging +import sys +import signal +import time +from bb.ui.crumbs.imageconfigurationpage import ImageConfigurationPage +from bb.ui.crumbs.recipeselectionpage import RecipeSelectionPage +from bb.ui.crumbs.packageselectionpage import PackageSelectionPage +from bb.ui.crumbs.builddetailspage import BuildDetailsPage +from bb.ui.crumbs.imagedetailspage import ImageDetailsPage +from bb.ui.crumbs.sanitycheckpage import SanityCheckPage +from bb.ui.crumbs.hobwidget import hwc, HobButton, HobAltButton +from bb.ui.crumbs.persistenttooltip import PersistentTooltip +import bb.ui.crumbs.utils +from bb.ui.crumbs.hig.crumbsmessagedialog import CrumbsMessageDialog +from bb.ui.crumbs.hig.simplesettingsdialog import SimpleSettingsDialog +from bb.ui.crumbs.hig.advancedsettingsdialog import AdvancedSettingsDialog +from bb.ui.crumbs.hig.deployimagedialog import DeployImageDialog +from bb.ui.crumbs.hig.layerselectiondialog import LayerSelectionDialog +from bb.ui.crumbs.hig.imageselectiondialog import ImageSelectionDialog +from bb.ui.crumbs.hig.parsingwarningsdialog import ParsingWarningsDialog +from bb.ui.crumbs.hig.propertydialog import PropertyDialog + +hobVer = 20120808 + +class Configuration: + '''Represents the data structure of configuration.''' + + @classmethod + def parse_proxy_string(cls, proxy): + pattern = "^\s*((http|https|ftp|socks|cvs)://)?((\S+):(\S+)@)?([^\s:]+)(:(\d+))?/?" + match = re.search(pattern, proxy) + if match: + return match.group(2), match.group(4), match.group(5), match.group(6), match.group(8) + else: + return None, None, None, "", "" + + @classmethod + def make_host_string(cls, prot, user, passwd, host, default_prot=""): + if host == None or host == "": + return "" + + passwd = passwd or "" + + if user != None and user != "": + if prot == None or prot == "": + prot = default_prot + return prot + "://" + user + ":" + passwd + "@" + host + else: + if prot == None or prot == "": + return host + else: + return prot + "://" + host + + @classmethod + def make_port_string(cls, port): + port = port or "" + return port + + @classmethod + def make_proxy_string(cls, prot, user, passwd, host, port, default_prot=""): + if host == None or host == "":# or port == None or port == "": + return "" + + return Configuration.make_host_string(prot, user, passwd, host, default_prot) + (":" + Configuration.make_port_string(port) if port else "") + + def __init__(self): + self.curr_mach = "" + self.selected_image = None + # settings + self.curr_distro = "" + self.dldir = self.sstatedir = self.sstatemirror = "" + self.pmake = self.bbthread = 0 + self.curr_package_format = "" + self.image_rootfs_size = self.image_extra_size = 0 + self.image_overhead_factor = 1 + self.incompat_license = "" + self.curr_sdk_machine = "" + self.conf_version = self.lconf_version = "" + self.extra_setting = {} + self.toolchain_build = False + self.image_fstypes = "" + self.image_size = None + self.image_packages = [] + # bblayers.conf + self.layers = [] + # image/recipes/packages + self.clear_selection() + + self.user_selected_packages = [] + + self.default_task = "build" + + # proxy settings + self.enable_proxy = None + self.same_proxy = False + self.proxies = { + "http" : [None, None, None, "", ""], # protocol : [prot, user, passwd, host, port] + "https" : [None, None, None, "", ""], + "ftp" : [None, None, None, "", ""], + "socks" : [None, None, None, "", ""], + "cvs" : [None, None, None, "", ""], + } + + def clear_selection(self): + self.selected_recipes = [] + self.selected_packages = [] + self.initial_selected_image = None + self.initial_selected_packages = [] + self.initial_user_selected_packages = [] + + def split_proxy(self, protocol, proxy): + entry = [] + prot, user, passwd, host, port = Configuration.parse_proxy_string(proxy) + entry.append(prot) + entry.append(user) + entry.append(passwd) + entry.append(host) + entry.append(port) + self.proxies[protocol] = entry + + def combine_proxy(self, protocol): + entry = self.proxies[protocol] + return Configuration.make_proxy_string(entry[0], entry[1], entry[2], entry[3], entry[4], protocol) + + def combine_host_only(self, protocol): + entry = self.proxies[protocol] + return Configuration.make_host_string(entry[0], entry[1], entry[2], entry[3], protocol) + + def combine_port_only(self, protocol): + entry = self.proxies[protocol] + return Configuration.make_port_string(entry[4]) + + def update(self, params): + # settings + self.curr_distro = params["distro"] + self.dldir = params["dldir"] + self.sstatedir = params["sstatedir"] + self.sstatemirror = params["sstatemirror"] + self.pmake = int(params["pmake"].split()[1]) + self.bbthread = params["bbthread"] + self.curr_package_format = " ".join(params["pclass"].split("package_")).strip() + self.image_rootfs_size = params["image_rootfs_size"] + self.image_extra_size = params["image_extra_size"] + self.image_overhead_factor = params['image_overhead_factor'] + self.incompat_license = params["incompat_license"] + self.curr_sdk_machine = params["sdk_machine"] + self.conf_version = params["conf_version"] + self.lconf_version = params["lconf_version"] + self.image_fstypes = params["image_fstypes"] + # self.extra_setting/self.toolchain_build + # bblayers.conf + self.layers = params["layer"].split() + self.layers_non_removable = params["layers_non_removable"].split() + self.default_task = params["default_task"] + + # proxy settings + self.enable_proxy = params["http_proxy"] != "" or params["https_proxy"] != "" \ + or params["ftp_proxy"] != "" or params["socks_proxy"] != "" \ + or params["cvs_proxy_host"] != "" or params["cvs_proxy_port"] != "" + self.split_proxy("http", params["http_proxy"]) + self.split_proxy("https", params["https_proxy"]) + self.split_proxy("ftp", params["ftp_proxy"]) + self.split_proxy("socks", params["socks_proxy"]) + self.split_proxy("cvs", params["cvs_proxy_host"] + ":" + params["cvs_proxy_port"]) + + def save(self, handler, defaults=False): + # bblayers.conf + handler.set_var_in_file("BBLAYERS", self.layers, "bblayers.conf") + # local.conf + if not defaults: + handler.early_assign_var_in_file("MACHINE", self.curr_mach, "local.conf") + handler.set_var_in_file("DISTRO", self.curr_distro, "local.conf") + handler.set_var_in_file("DL_DIR", self.dldir, "local.conf") + handler.set_var_in_file("SSTATE_DIR", self.sstatedir, "local.conf") + sstate_mirror_list = self.sstatemirror.split("\\n ") + sstate_mirror_list_modified = [] + for mirror in sstate_mirror_list: + if mirror != "": + mirror = mirror + "\\n" + sstate_mirror_list_modified.append(mirror) + handler.set_var_in_file("SSTATE_MIRRORS", sstate_mirror_list_modified, "local.conf") + handler.set_var_in_file("PARALLEL_MAKE", "-j %s" % self.pmake, "local.conf") + handler.set_var_in_file("BB_NUMBER_THREADS", self.bbthread, "local.conf") + handler.set_var_in_file("PACKAGE_CLASSES", " ".join(["package_" + i for i in self.curr_package_format.split()]), "local.conf") + handler.set_var_in_file("IMAGE_ROOTFS_SIZE", self.image_rootfs_size, "local.conf") + handler.set_var_in_file("IMAGE_EXTRA_SPACE", self.image_extra_size, "local.conf") + handler.set_var_in_file("INCOMPATIBLE_LICENSE", self.incompat_license, "local.conf") + handler.set_var_in_file("SDKMACHINE", self.curr_sdk_machine, "local.conf") + handler.set_var_in_file("CONF_VERSION", self.conf_version, "local.conf") + handler.set_var_in_file("LCONF_VERSION", self.lconf_version, "bblayers.conf") + handler.set_extra_config(self.extra_setting) + handler.set_var_in_file("TOOLCHAIN_BUILD", self.toolchain_build, "local.conf") + handler.set_var_in_file("IMAGE_FSTYPES", self.image_fstypes, "local.conf") + if not defaults: + # image/recipes/packages + handler.set_var_in_file("__SELECTED_IMAGE__", self.selected_image, "local.conf") + handler.set_var_in_file("DEPENDS", self.selected_recipes, "local.conf") + handler.set_var_in_file("IMAGE_INSTALL", self.user_selected_packages, "local.conf") + # proxy + if self.enable_proxy == True: + handler.set_var_in_file("http_proxy", self.combine_proxy("http"), "local.conf") + handler.set_var_in_file("https_proxy", self.combine_proxy("https"), "local.conf") + handler.set_var_in_file("ftp_proxy", self.combine_proxy("ftp"), "local.conf") + handler.set_var_in_file("all_proxy", self.combine_proxy("socks"), "local.conf") + handler.set_var_in_file("CVS_PROXY_HOST", self.combine_host_only("cvs"), "local.conf") + handler.set_var_in_file("CVS_PROXY_PORT", self.combine_port_only("cvs"), "local.conf") + else: + handler.set_var_in_file("http_proxy", "", "local.conf") + handler.set_var_in_file("https_proxy", "", "local.conf") + handler.set_var_in_file("ftp_proxy", "", "local.conf") + handler.set_var_in_file("all_proxy", "", "local.conf") + handler.set_var_in_file("CVS_PROXY_HOST", "", "local.conf") + handler.set_var_in_file("CVS_PROXY_PORT", "", "local.conf") + + def __str__(self): + s = "VERSION: '%s', BBLAYERS: '%s', MACHINE: '%s', DISTRO: '%s', DL_DIR: '%s'," % \ + (hobVer, " ".join(self.layers), self.curr_mach, self.curr_distro, self.dldir ) + s += "SSTATE_DIR: '%s', SSTATE_MIRROR: '%s', PARALLEL_MAKE: '-j %s', BB_NUMBER_THREADS: '%s', PACKAGE_CLASSES: '%s', " % \ + (self.sstatedir, self.sstatemirror, self.pmake, self.bbthread, " ".join(["package_" + i for i in self.curr_package_format.split()])) + s += "IMAGE_ROOTFS_SIZE: '%s', IMAGE_EXTRA_SPACE: '%s', INCOMPATIBLE_LICENSE: '%s', SDKMACHINE: '%s', CONF_VERSION: '%s', " % \ + (self.image_rootfs_size, self.image_extra_size, self.incompat_license, self.curr_sdk_machine, self.conf_version) + s += "LCONF_VERSION: '%s', EXTRA_SETTING: '%s', TOOLCHAIN_BUILD: '%s', IMAGE_FSTYPES: '%s', __SELECTED_IMAGE__: '%s', " % \ + (self.lconf_version, self.extra_setting, self.toolchain_build, self.image_fstypes, self.selected_image) + s += "DEPENDS: '%s', IMAGE_INSTALL: '%s', enable_proxy: '%s', use_same_proxy: '%s', http_proxy: '%s', " % \ + (self.selected_recipes, self.user_selected_packages, self.enable_proxy, self.same_proxy, self.combine_proxy("http")) + s += "https_proxy: '%s', ftp_proxy: '%s', all_proxy: '%s', CVS_PROXY_HOST: '%s', CVS_PROXY_PORT: '%s'" % \ + (self.combine_proxy("https"), self.combine_proxy("ftp"), self.combine_proxy("socks"), + self.combine_host_only("cvs"), self.combine_port_only("cvs")) + return s + +class Parameters: + '''Represents other variables like available machines, etc.''' + + def __init__(self): + # Variables + self.max_threads = 65535 + self.core_base = "" + self.image_addr = "" + self.image_types = [] + self.runnable_image_types = [] + self.runnable_machine_patterns = [] + self.deployable_image_types = [] + self.tmpdir = "" + + self.all_machines = [] + self.all_package_formats = [] + self.all_distros = [] + self.all_sdk_machines = [] + self.all_layers = [] + self.image_names = [] + self.image_white_pattern = "" + self.image_black_pattern = "" + + # for build log to show + self.bb_version = "" + self.target_arch = "" + self.target_os = "" + self.distro_version = "" + self.tune_pkgarch = "" + + def update(self, params): + self.max_threads = params["max_threads"] + self.core_base = params["core_base"] + self.image_addr = params["image_addr"] + self.image_types = params["image_types"].split() + self.runnable_image_types = params["runnable_image_types"].split() + self.runnable_machine_patterns = params["runnable_machine_patterns"].split() + self.deployable_image_types = params["deployable_image_types"].split() + self.tmpdir = params["tmpdir"] + self.image_white_pattern = params["image_white_pattern"] + self.image_black_pattern = params["image_black_pattern"] + self.kernel_image_type = params["kernel_image_type"] + # for build log to show + self.bb_version = params["bb_version"] + self.target_arch = params["target_arch"] + self.target_os = params["target_os"] + self.distro_version = params["distro_version"] + self.tune_pkgarch = params["tune_pkgarch"] + +def hob_conf_filter(fn, data): + if fn.endswith("/local.conf"): + distro = data.getVar("DISTRO_HOB") + if distro: + if distro != "defaultsetup": + data.setVar("DISTRO", distro) + else: + data.delVar("DISTRO") + + keys = ["MACHINE_HOB", "SDKMACHINE_HOB", "PACKAGE_CLASSES_HOB", \ + "BB_NUMBER_THREADS_HOB", "PARALLEL_MAKE_HOB", "DL_DIR_HOB", \ + "SSTATE_DIR_HOB", "SSTATE_MIRRORS_HOB", "INCOMPATIBLE_LICENSE_HOB"] + for key in keys: + var_hob = data.getVar(key) + if var_hob: + data.setVar(key.split("_HOB")[0], var_hob) + return + + if fn.endswith("/bblayers.conf"): + layers = data.getVar("BBLAYERS_HOB") + if layers: + data.setVar("BBLAYERS", layers) + return + +class Builder(gtk.Window): + + (INITIAL_CHECKS, + MACHINE_SELECTION, + RCPPKGINFO_POPULATING, + RCPPKGINFO_POPULATED, + BASEIMG_SELECTED, + RECIPE_SELECTION, + PACKAGE_GENERATING, + PACKAGE_GENERATED, + PACKAGE_SELECTION, + FAST_IMAGE_GENERATING, + IMAGE_GENERATING, + IMAGE_GENERATED, + MY_IMAGE_OPENED, + BACK, + END_NOOP) = range(15) + + (SANITY_CHECK, + IMAGE_CONFIGURATION, + RECIPE_DETAILS, + BUILD_DETAILS, + PACKAGE_DETAILS, + IMAGE_DETAILS, + END_TAB) = range(7) + + __step2page__ = { + INITIAL_CHECKS : SANITY_CHECK, + MACHINE_SELECTION : IMAGE_CONFIGURATION, + RCPPKGINFO_POPULATING : IMAGE_CONFIGURATION, + RCPPKGINFO_POPULATED : IMAGE_CONFIGURATION, + BASEIMG_SELECTED : IMAGE_CONFIGURATION, + RECIPE_SELECTION : RECIPE_DETAILS, + PACKAGE_GENERATING : BUILD_DETAILS, + PACKAGE_GENERATED : PACKAGE_DETAILS, + PACKAGE_SELECTION : PACKAGE_DETAILS, + FAST_IMAGE_GENERATING : BUILD_DETAILS, + IMAGE_GENERATING : BUILD_DETAILS, + IMAGE_GENERATED : IMAGE_DETAILS, + MY_IMAGE_OPENED : IMAGE_DETAILS, + END_NOOP : None, + } + + SANITY_CHECK_MIN_DISPLAY_TIME = 5 + + def __init__(self, hobHandler, recipe_model, package_model): + super(Builder, self).__init__() + + self.hob_image = "hob-image" + + # handler + self.handler = hobHandler + + # logger + self.logger = logging.getLogger("BitBake") + self.consolelog = None + self.current_logfile = None + + # configuration and parameters + self.configuration = Configuration() + self.parameters = Parameters() + + # build step + self.current_step = None + self.previous_step = None + + self.stopping = False + + # recipe model and package model + self.recipe_model = recipe_model + self.package_model = package_model + + # Indicate whether user has customized the image + self.customized = False + + # Indicate whether the UI is working + self.sensitive = True + + # Indicate whether the sanity check ran + self.sanity_checked = False + + # save parsing warnings + self.parsing_warnings = [] + + # create visual elements + self.create_visual_elements() + + # connect the signals to functions + self.connect("delete-event", self.destroy_window_cb) + self.recipe_model.connect ("recipe-selection-changed", self.recipelist_changed_cb) + self.package_model.connect("package-selection-changed", self.packagelist_changed_cb) + self.handler.connect("config-updated", self.handler_config_updated_cb) + self.handler.connect("package-formats-updated", self.handler_package_formats_updated_cb) + self.handler.connect("parsing-started", self.handler_parsing_started_cb) + self.handler.connect("parsing", self.handler_parsing_cb) + self.handler.connect("parsing-completed", self.handler_parsing_completed_cb) + self.handler.build.connect("build-started", self.handler_build_started_cb) + self.handler.build.connect("build-succeeded", self.handler_build_succeeded_cb) + self.handler.build.connect("build-failed", self.handler_build_failed_cb) + self.handler.build.connect("build-aborted", self.handler_build_aborted_cb) + self.handler.build.connect("task-started", self.handler_task_started_cb) + self.handler.build.connect("disk-full", self.handler_disk_full_cb) + self.handler.build.connect("log-error", self.handler_build_failure_cb) + self.handler.build.connect("log-warning", self.handler_build_failure_cb) + self.handler.build.connect("log", self.handler_build_log_cb) + self.handler.build.connect("no-provider", self.handler_no_provider_cb) + self.handler.connect("generating-data", self.handler_generating_data_cb) + self.handler.connect("data-generated", self.handler_data_generated_cb) + self.handler.connect("command-succeeded", self.handler_command_succeeded_cb) + self.handler.connect("command-failed", self.handler_command_failed_cb) + self.handler.connect("parsing-warning", self.handler_parsing_warning_cb) + self.handler.connect("sanity-failed", self.handler_sanity_failed_cb) + self.handler.connect("recipe-populated", self.handler_recipe_populated_cb) + self.handler.connect("package-populated", self.handler_package_populated_cb) + + self.handler.append_to_bbfiles("${TOPDIR}/recipes/images/custom/*.bb") + self.handler.append_to_bbfiles("${TOPDIR}/recipes/images/*.bb") + self.initiate_new_build_async() + + signal.signal(signal.SIGINT, self.event_handle_SIGINT) + + def create_visual_elements(self): + self.set_title("Hob") + self.set_icon_name("applications-development") + self.set_resizable(True) + + try: + window_width = self.get_screen().get_width() + window_height = self.get_screen().get_height() + except AttributeError: + print "Please set DISPLAY variable before running Hob." + sys.exit(1) + + if window_width >= hwc.MAIN_WIN_WIDTH: + window_width = hwc.MAIN_WIN_WIDTH + window_height = hwc.MAIN_WIN_HEIGHT + self.set_size_request(window_width, window_height) + + self.vbox = gtk.VBox(False, 0) + self.vbox.set_border_width(0) + self.add(self.vbox) + + # create pages + self.image_configuration_page = ImageConfigurationPage(self) + self.recipe_details_page = RecipeSelectionPage(self) + self.build_details_page = BuildDetailsPage(self) + self.package_details_page = PackageSelectionPage(self) + self.image_details_page = ImageDetailsPage(self) + self.sanity_check_page = SanityCheckPage(self) + self.display_sanity_check = False + self.sanity_check_post_func = False + self.had_network_error = False + + self.nb = gtk.Notebook() + self.nb.set_show_tabs(False) + self.nb.insert_page(self.sanity_check_page, None, self.SANITY_CHECK) + self.nb.insert_page(self.image_configuration_page, None, self.IMAGE_CONFIGURATION) + self.nb.insert_page(self.recipe_details_page, None, self.RECIPE_DETAILS) + self.nb.insert_page(self.build_details_page, None, self.BUILD_DETAILS) + self.nb.insert_page(self.package_details_page, None, self.PACKAGE_DETAILS) + self.nb.insert_page(self.image_details_page, None, self.IMAGE_DETAILS) + self.vbox.pack_start(self.nb, expand=True, fill=True) + + self.show_all() + self.nb.set_current_page(0) + + def sanity_check_timeout(self): + # The minimum time for showing the 'sanity check' page has passe + # If someone set the 'sanity_check_post_step' meanwhile, execute it now + self.display_sanity_check = False + if self.sanity_check_post_func: + temp = self.sanity_check_post_func + self.sanity_check_post_func = None + temp() + return False + + def show_sanity_check_page(self): + # This window must stay on screen for at least 5 seconds, according to the design document + self.nb.set_current_page(self.SANITY_CHECK) + self.sanity_check_post_step = None + self.display_sanity_check = True + self.sanity_check_page.start() + gobject.timeout_add(self.SANITY_CHECK_MIN_DISPLAY_TIME * 1000, self.sanity_check_timeout) + + def execute_after_sanity_check(self, func): + if not self.display_sanity_check: + func() + else: + self.sanity_check_post_func = func + + def generate_configuration(self): + if not self.sanity_checked: + self.show_sanity_check_page() + self.handler.generate_configuration() + + def initiate_new_build_async(self): + self.configuration.selected_image = None + self.switch_page(self.MACHINE_SELECTION) + self.handler.init_cooker() + self.handler.set_extra_inherit("image_types") + self.generate_configuration() + + def update_config_async(self): + self.set_user_config() + self.generate_configuration() + self.switch_page(self.MACHINE_SELECTION) + + def sanity_check(self): + self.handler.trigger_sanity_check() + + def populate_recipe_package_info_async(self): + self.switch_page(self.RCPPKGINFO_POPULATING) + # Parse recipes + self.set_user_config() + self.handler.generate_recipes() + + def generate_packages_async(self, log = False): + self.switch_page(self.PACKAGE_GENERATING) + if log: + self.current_logfile = self.handler.get_logfile() + self.do_log(self.current_logfile) + # Build packages + _, all_recipes = self.recipe_model.get_selected_recipes() + self.set_user_config() + self.handler.reset_build() + self.handler.generate_packages(all_recipes, self.configuration.default_task) + + def restore_initial_selected_packages(self): + self.package_model.set_selected_packages(self.configuration.initial_user_selected_packages, True) + self.package_model.set_selected_packages(self.configuration.initial_selected_packages) + for package in self.configuration.selected_packages: + if package not in self.configuration.initial_selected_packages: + self.package_model.exclude_item(self.package_model.find_path_for_item(package)) + + def fast_generate_image_async(self, log = False): + self.switch_page(self.FAST_IMAGE_GENERATING) + if log: + self.current_logfile = self.handler.get_logfile() + self.do_log(self.current_logfile) + # Build packages + _, all_recipes = self.recipe_model.get_selected_recipes() + self.set_user_config() + self.handler.reset_build() + self.handler.generate_packages(all_recipes, self.configuration.default_task) + + def generate_image_async(self, cont = False): + self.switch_page(self.IMAGE_GENERATING) + self.handler.reset_build() + if not cont: + self.current_logfile = self.handler.get_logfile() + self.do_log(self.current_logfile) + # Build image + self.set_user_config() + toolchain_packages = [] + base_image = None + if self.configuration.toolchain_build: + toolchain_packages = self.package_model.get_selected_packages_toolchain() + if self.configuration.selected_image == self.recipe_model.__custom_image__: + packages = self.package_model.get_selected_packages() + image = self.hob_image + base_image = self.configuration.initial_selected_image + else: + packages = [] + image = self.configuration.selected_image + self.handler.generate_image(image, + base_image, + packages, + toolchain_packages, + self.configuration.default_task) + + def generate_new_image(self, image, description): + base_image = self.configuration.initial_selected_image + if base_image == self.recipe_model.__custom_image__: + base_image = None + packages = self.package_model.get_selected_packages() + self.handler.generate_new_image(image, base_image, packages, description) + + def ensure_dir(self, directory): + self.handler.ensure_dir(directory) + + def get_parameters_sync(self): + return self.handler.get_parameters() + + def request_package_info_async(self): + self.handler.request_package_info() + + def cancel_build_sync(self, force=False): + self.handler.cancel_build(force) + + def cancel_parse_sync(self): + self.handler.cancel_parse() + + def switch_page(self, next_step): + # Main Workflow (Business Logic) + self.nb.set_current_page(self.__step2page__[next_step]) + + if next_step == self.MACHINE_SELECTION: # init step + self.image_configuration_page.show_machine() + + elif next_step == self.RCPPKGINFO_POPULATING: + # MACHINE CHANGED action or SETTINGS CHANGED + # show the progress bar + self.image_configuration_page.show_info_populating() + + elif next_step == self.RCPPKGINFO_POPULATED: + self.image_configuration_page.show_info_populated() + + elif next_step == self.BASEIMG_SELECTED: + self.image_configuration_page.show_baseimg_selected() + + elif next_step == self.RECIPE_SELECTION: + if self.recipe_model.get_selected_image() == self.recipe_model.__custom_image__: + self.recipe_details_page.set_recipe_curr_tab(self.recipe_details_page.ALL) + else: + self.recipe_details_page.set_recipe_curr_tab(self.recipe_details_page.INCLUDED) + + elif next_step == self.PACKAGE_SELECTION: + self.configuration.initial_selected_packages = self.configuration.selected_packages + self.configuration.initial_user_selected_packages = self.configuration.user_selected_packages + self.package_details_page.set_title("Edit packages") + if self.recipe_model.get_selected_image() == self.recipe_model.__custom_image__: + self.package_details_page.set_packages_curr_tab(self.package_details_page.ALL) + else: + self.package_details_page.set_packages_curr_tab(self.package_details_page.INCLUDED) + self.package_details_page.show_page(self.current_logfile) + + + elif next_step == self.PACKAGE_GENERATING or next_step == self.FAST_IMAGE_GENERATING: + # both PACKAGE_GENERATING and FAST_IMAGE_GENERATING share the same page + self.build_details_page.show_page(next_step) + + elif next_step == self.PACKAGE_GENERATED: + self.package_details_page.set_title("Step 2 of 2: Edit packages") + if self.recipe_model.get_selected_image() == self.recipe_model.__custom_image__: + self.package_details_page.set_packages_curr_tab(self.package_details_page.ALL) + else: + self.package_details_page.set_packages_curr_tab(self.package_details_page.INCLUDED) + self.package_details_page.show_page(self.current_logfile) + + elif next_step == self.IMAGE_GENERATING: + # after packages are generated, selected_packages need to + # be updated in package_model per selected_image in recipe_model + self.build_details_page.show_page(next_step) + + elif next_step == self.IMAGE_GENERATED: + self.image_details_page.show_page(next_step) + + elif next_step == self.MY_IMAGE_OPENED: + self.image_details_page.show_page(next_step) + + self.previous_step = self.current_step + self.current_step = next_step + + def set_user_config_proxies(self): + if self.configuration.enable_proxy == True: + self.handler.set_http_proxy(self.configuration.combine_proxy("http")) + self.handler.set_https_proxy(self.configuration.combine_proxy("https")) + self.handler.set_ftp_proxy(self.configuration.combine_proxy("ftp")) + self.handler.set_socks_proxy(self.configuration.combine_proxy("socks")) + self.handler.set_cvs_proxy(self.configuration.combine_host_only("cvs"), self.configuration.combine_port_only("cvs")) + elif self.configuration.enable_proxy == False: + self.handler.set_http_proxy("") + self.handler.set_https_proxy("") + self.handler.set_ftp_proxy("") + self.handler.set_socks_proxy("") + self.handler.set_cvs_proxy("", "") + + def set_user_config_extra(self): + self.handler.set_rootfs_size(self.configuration.image_rootfs_size) + self.handler.set_extra_size(self.configuration.image_extra_size) + self.handler.set_incompatible_license(self.configuration.incompat_license) + self.handler.set_sdk_machine(self.configuration.curr_sdk_machine) + self.handler.set_image_fstypes(self.configuration.image_fstypes) + self.handler.set_extra_config(self.configuration.extra_setting) + self.handler.set_extra_inherit("packageinfo image_types") + self.set_user_config_proxies() + + def set_user_config(self): + # set bb layers + self.handler.set_bblayers(self.configuration.layers) + # set local configuration + self.handler.set_machine(self.configuration.curr_mach) + self.handler.set_package_format(self.configuration.curr_package_format) + self.handler.set_distro(self.configuration.curr_distro) + self.handler.set_dl_dir(self.configuration.dldir) + self.handler.set_sstate_dir(self.configuration.sstatedir) + self.handler.set_sstate_mirrors(self.configuration.sstatemirror) + self.handler.set_pmake(self.configuration.pmake) + self.handler.set_bbthreads(self.configuration.bbthread) + self.set_user_config_extra() + + def update_recipe_model(self, selected_image, selected_recipes): + self.recipe_model.set_selected_image(selected_image) + self.recipe_model.set_selected_recipes(selected_recipes) + + def update_package_model(self, selected_packages, user_selected_packages=None): + if user_selected_packages: + left = self.package_model.set_selected_packages(user_selected_packages, True) + self.configuration.user_selected_packages += left + left = self.package_model.set_selected_packages(selected_packages) + self.configuration.selected_packages += left + + def update_configuration_parameters(self, params): + if params: + self.configuration.update(params) + self.parameters.update(params) + + def set_base_image(self): + self.configuration.initial_selected_image = self.configuration.selected_image + if self.configuration.selected_image != self.recipe_model.__custom_image__: + self.hob_image = self.configuration.selected_image + "-edited" + + def reset(self): + self.configuration.curr_mach = "" + self.configuration.clear_selection() + self.image_configuration_page.switch_machine_combo() + self.switch_page(self.MACHINE_SELECTION) + + # Callback Functions + def handler_config_updated_cb(self, handler, which, values): + if which == "distro": + self.parameters.all_distros = values + elif which == "machine": + self.parameters.all_machines = values + self.image_configuration_page.update_machine_combo() + elif which == "machine-sdk": + self.parameters.all_sdk_machines = values + + def handler_package_formats_updated_cb(self, handler, formats): + self.parameters.all_package_formats = formats + + def switch_to_image_configuration_helper(self): + self.sanity_check_page.stop() + self.switch_page(self.IMAGE_CONFIGURATION) + self.image_configuration_page.switch_machine_combo() + + def show_network_error_dialog_helper(self): + self.sanity_check_page.stop() + self.show_network_error_dialog() + + def handler_command_succeeded_cb(self, handler, initcmd): + if initcmd == self.handler.GENERATE_CONFIGURATION: + if not self.configuration.curr_mach: + self.configuration.curr_mach = self.handler.runCommand(["getVariable", "HOB_MACHINE"]) or "" + self.update_configuration_parameters(self.get_parameters_sync()) + if not self.sanity_checked: + self.sanity_check() + self.sanity_checked = True + elif initcmd == self.handler.SANITY_CHECK: + if self.had_network_error: + self.had_network_error = False + self.execute_after_sanity_check(self.show_network_error_dialog_helper) + else: + # Switch to the 'image configuration' page now, but we might need + # to wait for the minimum display time of the sanity check page + self.execute_after_sanity_check(self.switch_to_image_configuration_helper) + elif initcmd in [self.handler.GENERATE_RECIPES, + self.handler.GENERATE_PACKAGES, + self.handler.GENERATE_IMAGE]: + self.update_configuration_parameters(self.get_parameters_sync()) + self.request_package_info_async() + elif initcmd == self.handler.POPULATE_PACKAGEINFO: + if self.current_step == self.RCPPKGINFO_POPULATING: + self.switch_page(self.RCPPKGINFO_POPULATED) + self.rcppkglist_populated() + return + + self.rcppkglist_populated() + if self.current_step == self.FAST_IMAGE_GENERATING: + self.generate_image_async(True) + + def show_error_dialog(self, msg): + lbl = "Hob found an error" + dialog = CrumbsMessageDialog(self, lbl, gtk.MESSAGE_ERROR, msg) + button = dialog.add_button("Close", gtk.RESPONSE_OK) + HobButton.style_button(button) + response = dialog.run() + dialog.destroy() + + def show_warning_dialog(self): + dialog = ParsingWarningsDialog(title = "View warnings", + warnings = self.parsing_warnings, + parent = None, + flags = gtk.DIALOG_DESTROY_WITH_PARENT + | gtk.DIALOG_NO_SEPARATOR) + response = dialog.run() + dialog.destroy() + + def show_network_error_dialog(self): + lbl = "Hob cannot connect to the network" + msg = msg + "Please check your network connection. If you are using a proxy server, please make sure it is configured correctly." + dialog = CrumbsMessageDialog(self, lbl, gtk.MESSAGE_ERROR, msg) + button = dialog.add_button("Close", gtk.RESPONSE_OK) + HobButton.style_button(button) + button = dialog.add_button("Proxy settings", gtk.RESPONSE_CANCEL) + HobButton.style_button(button) + res = dialog.run() + dialog.destroy() + if res == gtk.RESPONSE_CANCEL: + res, settings_changed = self.show_simple_settings_dialog(SimpleSettingsDialog.PROXIES_PAGE_ID) + if not res: + return + if settings_changed: + self.reparse_post_adv_settings() + + def handler_command_failed_cb(self, handler, msg): + if msg: + self.show_error_dialog(msg) + self.reset() + + def handler_parsing_warning_cb(self, handler, warn_msg): + self.parsing_warnings.append(warn_msg) + + def handler_sanity_failed_cb(self, handler, msg, network_error): + self.reset() + if network_error: + # Mark this in an internal field. The "network error" dialog will be + # shown later, when a SanityCheckPassed event will be handled + # (as sent by sanity.bbclass) + self.had_network_error = True + else: + msg = msg.replace("your local.conf", "Settings") + self.show_error_dialog(msg) + self.reset() + + def window_sensitive(self, sensitive): + self.image_configuration_page.machine_combo.set_sensitive(sensitive) + self.image_configuration_page.machine_combo.child.set_sensitive(sensitive) + self.image_configuration_page.image_combo.set_sensitive(sensitive) + self.image_configuration_page.image_combo.child.set_sensitive(sensitive) + self.image_configuration_page.layer_button.set_sensitive(sensitive) + self.image_configuration_page.layer_info_icon.set_sensitive(sensitive) + self.image_configuration_page.toolbar.set_sensitive(sensitive) + self.image_configuration_page.view_adv_configuration_button.set_sensitive(sensitive) + self.image_configuration_page.config_build_button.set_sensitive(sensitive) + + self.recipe_details_page.set_sensitive(sensitive) + self.package_details_page.set_sensitive(sensitive) + self.build_details_page.set_sensitive(sensitive) + self.image_details_page.set_sensitive(sensitive) + + if sensitive: + self.window.set_cursor(None) + else: + self.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.WATCH)) + self.sensitive = sensitive + + + def handler_generating_data_cb(self, handler): + self.window_sensitive(False) + + def handler_data_generated_cb(self, handler): + self.window_sensitive(True) + + def rcppkglist_populated(self): + selected_image = self.configuration.selected_image + selected_recipes = self.configuration.selected_recipes[:] + selected_packages = self.configuration.selected_packages[:] + user_selected_packages = self.configuration.user_selected_packages[:] + + self.image_configuration_page.update_image_combo(self.recipe_model, selected_image) + self.image_configuration_page.update_image_desc() + self.update_recipe_model(selected_image, selected_recipes) + self.update_package_model(selected_packages, user_selected_packages) + + def recipelist_changed_cb(self, recipe_model): + self.recipe_details_page.refresh_selection() + + def packagelist_changed_cb(self, package_model): + self.package_details_page.refresh_selection() + + def handler_recipe_populated_cb(self, handler): + self.image_configuration_page.update_progress_bar("Populating recipes", 0.99) + + def handler_package_populated_cb(self, handler): + self.image_configuration_page.update_progress_bar("Populating packages", 1.0) + + def handler_parsing_started_cb(self, handler, message): + if self.current_step != self.RCPPKGINFO_POPULATING: + return + + fraction = 0 + if message["eventname"] == "TreeDataPreparationStarted": + fraction = 0.6 + fraction + self.image_configuration_page.stop_button.set_sensitive(False) + self.image_configuration_page.update_progress_bar("Generating dependency tree", fraction) + else: + self.image_configuration_page.stop_button.set_sensitive(True) + self.image_configuration_page.update_progress_bar(message["title"], fraction) + + def handler_parsing_cb(self, handler, message): + if self.current_step != self.RCPPKGINFO_POPULATING: + return + + fraction = message["current"] * 1.0/message["total"] + if message["eventname"] == "TreeDataPreparationProgress": + fraction = 0.6 + 0.38 * fraction + self.image_configuration_page.update_progress_bar("Generating dependency tree", fraction) + else: + fraction = 0.6 * fraction + self.image_configuration_page.update_progress_bar(message["title"], fraction) + + def handler_parsing_completed_cb(self, handler, message): + if self.current_step != self.RCPPKGINFO_POPULATING: + return + + if message["eventname"] == "TreeDataPreparationCompleted": + fraction = 0.98 + else: + fraction = 0.6 + self.image_configuration_page.update_progress_bar("Generating dependency tree", fraction) + + def handler_build_started_cb(self, running_build): + if self.current_step == self.FAST_IMAGE_GENERATING: + fraction = 0 + elif self.current_step == self.IMAGE_GENERATING: + if self.previous_step == self.FAST_IMAGE_GENERATING: + fraction = 0.9 + else: + fraction = 0 + elif self.current_step == self.PACKAGE_GENERATING: + fraction = 0 + self.build_details_page.update_progress_bar("Build Started: ", fraction) + self.build_details_page.show_configurations(self.configuration, self.parameters) + + def build_succeeded(self): + if self.current_step == self.FAST_IMAGE_GENERATING: + fraction = 0.9 + elif self.current_step == self.IMAGE_GENERATING: + fraction = 1.0 + version = "" + self.parameters.image_names = [] + selected_image = self.recipe_model.get_selected_image() + if selected_image == self.recipe_model.__custom_image__: + if self.configuration.initial_selected_image != selected_image: + version = self.recipe_model.get_custom_image_version() + linkname = self.hob_image + version + "-" + self.configuration.curr_mach + else: + linkname = selected_image + '-' + self.configuration.curr_mach + image_extension = self.get_image_extension() + for image_type in self.parameters.image_types: + if image_type in image_extension: + real_types = image_extension[image_type] + else: + real_types = [image_type] + for real_image_type in real_types: + linkpath = self.parameters.image_addr + '/' + linkname + '.' + real_image_type + if os.path.exists(linkpath): + self.parameters.image_names.append(os.readlink(linkpath)) + elif self.current_step == self.PACKAGE_GENERATING: + fraction = 1.0 + self.build_details_page.update_progress_bar("Build Completed: ", fraction) + self.handler.build_succeeded_async() + self.stopping = False + + if self.current_step == self.PACKAGE_GENERATING: + self.switch_page(self.PACKAGE_GENERATED) + elif self.current_step == self.IMAGE_GENERATING: + self.switch_page(self.IMAGE_GENERATED) + + def build_failed(self): + if self.stopping: + status = "stop" + message = "Build stopped: " + fraction = self.build_details_page.progress_bar.get_fraction() + stop_to_next_edit = "" + if self.current_step == self.FAST_IMAGE_GENERATING: + stop_to_next_edit = "image configuration" + elif self.current_step == self.IMAGE_GENERATING: + if self.previous_step == self.FAST_IMAGE_GENERATING: + stop_to_next_edit = "image configuration" + else: + stop_to_next_edit = "packages" + elif self.current_step == self.PACKAGE_GENERATING: + stop_to_next_edit = "recipes" + button = self.build_details_page.show_stop_page(stop_to_next_edit.split(' ')[0]) + self.set_default(button) + else: + fail_to_next_edit = "" + if self.current_step == self.FAST_IMAGE_GENERATING: + fail_to_next_edit = "image configuration" + fraction = 0.9 + elif self.current_step == self.IMAGE_GENERATING: + if self.previous_step == self.FAST_IMAGE_GENERATING: + fail_to_next_edit = "image configuration" + else: + fail_to_next_edit = "packages" + fraction = 1.0 + elif self.current_step == self.PACKAGE_GENERATING: + fail_to_next_edit = "recipes" + fraction = 1.0 + self.build_details_page.show_fail_page(fail_to_next_edit.split(' ')[0]) + status = "fail" + message = "Build failed: " + self.build_details_page.update_progress_bar(message, fraction, status) + self.build_details_page.show_back_button() + self.build_details_page.hide_stop_button() + self.handler.build_failed_async() + self.stopping = False + + def handler_build_succeeded_cb(self, running_build): + if not self.stopping: + self.build_succeeded() + else: + self.build_failed() + + + def handler_build_failed_cb(self, running_build): + self.build_failed() + + def handler_build_aborted_cb(self, running_build): + self.build_failed() + + def handler_no_provider_cb(self, running_build, msg): + dialog = CrumbsMessageDialog(self, glib.markup_escape_text(msg), gtk.MESSAGE_INFO) + button = dialog.add_button("Close", gtk.RESPONSE_OK) + HobButton.style_button(button) + dialog.run() + dialog.destroy() + self.build_failed() + + def handler_task_started_cb(self, running_build, message): + fraction = message["current"] * 1.0/message["total"] + title = "Build packages" + if self.current_step == self.FAST_IMAGE_GENERATING: + if message["eventname"] == "sceneQueueTaskStarted": + fraction = 0.27 * fraction + elif message["eventname"] == "runQueueTaskStarted": + fraction = 0.27 + 0.63 * fraction + elif self.current_step == self.IMAGE_GENERATING: + title = "Build image" + if self.previous_step == self.FAST_IMAGE_GENERATING: + if message["eventname"] == "sceneQueueTaskStarted": + fraction = 0.27 + 0.63 + 0.03 * fraction + elif message["eventname"] == "runQueueTaskStarted": + fraction = 0.27 + 0.63 + 0.03 + 0.07 * fraction + else: + if message["eventname"] == "sceneQueueTaskStarted": + fraction = 0.2 * fraction + elif message["eventname"] == "runQueueTaskStarted": + fraction = 0.2 + 0.8 * fraction + elif self.current_step == self.PACKAGE_GENERATING: + if message["eventname"] == "sceneQueueTaskStarted": + fraction = 0.2 * fraction + elif message["eventname"] == "runQueueTaskStarted": + fraction = 0.2 + 0.8 * fraction + self.build_details_page.update_progress_bar(title + ": ", fraction) + self.build_details_page.update_build_status(message["current"], message["total"], message["task"]) + + def handler_disk_full_cb(self, running_build): + self.disk_full = True + + def handler_build_failure_cb(self, running_build): + self.build_details_page.show_issues() + + def handler_build_log_cb(self, running_build, func, obj): + if hasattr(self.logger, func): + getattr(self.logger, func)(obj) + + def destroy_window_cb(self, widget, event): + if not self.sensitive: + return True + elif self.handler.building: + self.stop_build() + return True + else: + gtk.main_quit() + + def event_handle_SIGINT(self, signal, frame): + for w in gtk.window_list_toplevels(): + if w.get_modal(): + w.response(gtk.RESPONSE_DELETE_EVENT) + sys.exit(0) + + def build_packages(self): + _, all_recipes = self.recipe_model.get_selected_recipes() + if not all_recipes: + lbl = "No selections made" + msg = "You have not made any selections" + msg = msg + " so there isn't anything to bake at this time." + dialog = CrumbsMessageDialog(self, lbl, gtk.MESSAGE_INFO, msg) + button = dialog.add_button("Close", gtk.RESPONSE_OK) + HobButton.style_button(button) + dialog.run() + dialog.destroy() + return + self.generate_packages_async(True) + + def build_image(self): + selected_packages = self.package_model.get_selected_packages() + if not selected_packages: + lbl = "No selections made" + msg = "You have not made any selections" + msg = msg + " so there isn't anything to bake at this time." + dialog = CrumbsMessageDialog(self, lbl, gtk.MESSAGE_INFO, msg) + button = dialog.add_button("Close", gtk.RESPONSE_OK) + HobButton.style_button(button) + dialog.run() + dialog.destroy() + return + self.generate_image_async(True) + + def just_bake(self): + selected_image = self.recipe_model.get_selected_image() + selected_packages = self.package_model.get_selected_packages() or [] + + # If no base image and no selected packages don't build anything + if not (selected_packages or selected_image != self.recipe_model.__custom_image__): + lbl = "No selections made" + msg = "You have not made any selections" + msg = msg + " so there isn't anything to bake at this time." + dialog = CrumbsMessageDialog(self, lbl, gtk.MESSAGE_INFO, msg) + button = dialog.add_button("Close", gtk.RESPONSE_OK) + HobButton.style_button(button) + dialog.run() + dialog.destroy() + return + + self.fast_generate_image_async(True) + + def show_recipe_property_dialog(self, properties): + information = {} + dialog = PropertyDialog(title = properties["name"] +' '+ "properties", + parent = self, + information = properties, + flags = gtk.DIALOG_DESTROY_WITH_PARENT + | gtk.DIALOG_NO_SEPARATOR) + + dialog.set_modal(False) + + button = dialog.add_button("Close", gtk.RESPONSE_NO) + HobAltButton.style_button(button) + button.connect("clicked", lambda w: dialog.destroy()) + + dialog.run() + + def show_packages_property_dialog(self, properties): + information = {} + dialog = PropertyDialog(title = properties["name"] +' '+ "properties", + parent = self, + information = properties, + flags = gtk.DIALOG_DESTROY_WITH_PARENT + | gtk.DIALOG_NO_SEPARATOR) + + dialog.set_modal(False) + + button = dialog.add_button("Close", gtk.RESPONSE_NO) + HobAltButton.style_button(button) + button.connect("clicked", lambda w: dialog.destroy()) + + dialog.run() + + def show_layer_selection_dialog(self): + dialog = LayerSelectionDialog(title = "Layers", + layers = copy.deepcopy(self.configuration.layers), + layers_non_removable = copy.deepcopy(self.configuration.layers_non_removable), + all_layers = self.parameters.all_layers, + parent = self, + flags = gtk.DIALOG_MODAL + | gtk.DIALOG_DESTROY_WITH_PARENT + | gtk.DIALOG_NO_SEPARATOR) + button = dialog.add_button("Cancel", gtk.RESPONSE_NO) + HobAltButton.style_button(button) + button = dialog.add_button("OK", gtk.RESPONSE_YES) + HobButton.style_button(button) + response = dialog.run() + if response == gtk.RESPONSE_YES: + self.configuration.layers = dialog.layers + # DO refresh layers + if dialog.layers_changed: + self.update_config_async() + dialog.destroy() + + def get_image_extension(self): + image_extension = {} + for type in self.parameters.image_types: + ext = self.handler.runCommand(["getVariable", "IMAGE_EXTENSION_%s" % type]) + if ext: + image_extension[type] = ext.split(' ') + + return image_extension + + def show_load_my_images_dialog(self): + image_extension = self.get_image_extension() + dialog = ImageSelectionDialog(self.parameters.image_addr, self.parameters.image_types, + "Open My Images", self, + gtk.FILE_CHOOSER_ACTION_SAVE, None, + image_extension) + button = dialog.add_button("Cancel", gtk.RESPONSE_NO) + HobAltButton.style_button(button) + button = dialog.add_button("Open", gtk.RESPONSE_YES) + HobButton.style_button(button) + response = dialog.run() + if response == gtk.RESPONSE_YES: + if not dialog.image_names: + lbl = "No selections made" + msg = "You have not made any selections" + crumbs_dialog = CrumbsMessageDialog(self, lbl, gtk.MESSAGE_INFO, msg) + button = crumbs_dialog.add_button("Close", gtk.RESPONSE_OK) + HobButton.style_button(button) + crumbs_dialog.run() + crumbs_dialog.destroy() + dialog.destroy() + return + + self.parameters.image_addr = dialog.image_folder + self.parameters.image_names = dialog.image_names[:] + self.switch_page(self.MY_IMAGE_OPENED) + + dialog.destroy() + + def show_adv_settings_dialog(self, tab=None): + dialog = AdvancedSettingsDialog(title = "Advanced configuration", + configuration = copy.deepcopy(self.configuration), + all_image_types = self.parameters.image_types, + all_package_formats = self.parameters.all_package_formats, + all_distros = self.parameters.all_distros, + all_sdk_machines = self.parameters.all_sdk_machines, + max_threads = self.parameters.max_threads, + parent = self, + flags = gtk.DIALOG_MODAL + | gtk.DIALOG_DESTROY_WITH_PARENT + | gtk.DIALOG_NO_SEPARATOR) + button = dialog.add_button("Cancel", gtk.RESPONSE_NO) + HobAltButton.style_button(button) + button = dialog.add_button("Save", gtk.RESPONSE_YES) + HobButton.style_button(button) + dialog.set_save_button(button) + response = dialog.run() + settings_changed = False + if response == gtk.RESPONSE_YES: + self.configuration = dialog.configuration + self.configuration.save(self.handler, True) # remember settings + settings_changed = dialog.settings_changed + dialog.destroy() + return response == gtk.RESPONSE_YES, settings_changed + + def show_simple_settings_dialog(self, tab=None): + dialog = SimpleSettingsDialog(title = "Settings", + configuration = copy.deepcopy(self.configuration), + all_image_types = self.parameters.image_types, + all_package_formats = self.parameters.all_package_formats, + all_distros = self.parameters.all_distros, + all_sdk_machines = self.parameters.all_sdk_machines, + max_threads = self.parameters.max_threads, + parent = self, + flags = gtk.DIALOG_MODAL + | gtk.DIALOG_DESTROY_WITH_PARENT + | gtk.DIALOG_NO_SEPARATOR, + handler = self.handler) + button = dialog.add_button("Cancel", gtk.RESPONSE_NO) + HobAltButton.style_button(button) + button = dialog.add_button("Save", gtk.RESPONSE_YES) + HobButton.style_button(button) + if tab: + dialog.switch_to_page(tab) + response = dialog.run() + settings_changed = False + if response == gtk.RESPONSE_YES: + self.configuration = dialog.configuration + self.configuration.save(self.handler, True) # remember settings + settings_changed = dialog.settings_changed + if dialog.proxy_settings_changed: + self.set_user_config_proxies() + elif dialog.proxy_test_ran: + # The user might have modified the proxies in the "Proxy" + # tab, which in turn made the proxy settings modify in bb. + # If "Cancel" was pressed, restore the previous proxy + # settings inside bb. + self.set_user_config_proxies() + dialog.destroy() + return response == gtk.RESPONSE_YES, settings_changed + + def reparse_post_adv_settings(self): + if not self.configuration.curr_mach: + self.update_config_async() + else: + self.configuration.clear_selection() + # DO reparse recipes + self.populate_recipe_package_info_async() + + def deploy_image(self, image_name): + if not image_name: + lbl = "Please select an image to deploy." + dialog = CrumbsMessageDialog(self, lbl, gtk.MESSAGE_INFO) + button = dialog.add_button("Close", gtk.RESPONSE_OK) + HobButton.style_button(button) + dialog.run() + dialog.destroy() + return + + image_path = os.path.join(self.parameters.image_addr, image_name) + dialog = DeployImageDialog(title = "Usb Image Maker", + image_path = image_path, + parent = self, + flags = gtk.DIALOG_MODAL + | gtk.DIALOG_DESTROY_WITH_PARENT + | gtk.DIALOG_NO_SEPARATOR) + button = dialog.add_button("Close", gtk.RESPONSE_NO) + HobAltButton.style_button(button) + button = dialog.add_button("Make usb image", gtk.RESPONSE_YES) + HobButton.style_button(button) + response = dialog.run() + dialog.destroy() + + def show_load_kernel_dialog(self): + dialog = gtk.FileChooserDialog("Load Kernel Files", self, + gtk.FILE_CHOOSER_ACTION_SAVE) + button = dialog.add_button("Cancel", gtk.RESPONSE_NO) + HobAltButton.style_button(button) + button = dialog.add_button("Open", gtk.RESPONSE_YES) + HobButton.style_button(button) + filter = gtk.FileFilter() + filter.set_name("Kernel Files") + filter.add_pattern("*.bin") + dialog.add_filter(filter) + + dialog.set_current_folder(self.parameters.image_addr) + + response = dialog.run() + kernel_path = "" + if response == gtk.RESPONSE_YES: + kernel_path = dialog.get_filename() + + dialog.destroy() + + return kernel_path + + def runqemu_image(self, image_name, kernel_name): + if not image_name or not kernel_name: + lbl = "Please select %s to launch in QEMU." % ("a kernel" if image_name else "an image") + dialog = CrumbsMessageDialog(self, lbl, gtk.MESSAGE_INFO) + button = dialog.add_button("Close", gtk.RESPONSE_OK) + HobButton.style_button(button) + dialog.run() + dialog.destroy() + return + + kernel_path = os.path.join(self.parameters.image_addr, kernel_name) + image_path = os.path.join(self.parameters.image_addr, image_name) + + source_env_path = os.path.join(self.parameters.core_base, "oe-init-build-env") + tmp_path = self.parameters.tmpdir + cmdline = bb.ui.crumbs.utils.which_terminal() + if os.path.exists(image_path) and os.path.exists(kernel_path) \ + and os.path.exists(source_env_path) and os.path.exists(tmp_path) \ + and cmdline: + cmdline += "\' bash -c \"export OE_TMPDIR=" + tmp_path + "; " + cmdline += "source " + source_env_path + " " + os.getcwd() + "; " + cmdline += "runqemu " + kernel_path + " " + image_path + "\"\'" + subprocess.Popen(shlex.split(cmdline)) + else: + lbl = "Path error" + msg = "One of your paths is wrong," + msg = msg + " please make sure the following paths exist:\n" + msg = msg + "image path:" + image_path + "\n" + msg = msg + "kernel path:" + kernel_path + "\n" + msg = msg + "source environment path:" + source_env_path + "\n" + msg = msg + "tmp path: " + tmp_path + "." + msg = msg + "You may be missing either xterm or vte for terminal services." + dialog = CrumbsMessageDialog(self, lbl, gtk.MESSAGE_ERROR, msg) + button = dialog.add_button("Close", gtk.RESPONSE_OK) + HobButton.style_button(button) + dialog.run() + dialog.destroy() + + def show_packages(self): + self.package_details_page.refresh_tables() + self.switch_page(self.PACKAGE_SELECTION) + + def show_recipes(self): + self.switch_page(self.RECIPE_SELECTION) + + def show_image_details(self): + self.switch_page(self.IMAGE_GENERATED) + + def show_configuration(self): + self.switch_page(self.BASEIMG_SELECTED) + + def stop_build(self): + if self.stopping: + lbl = "Force Stop build?" + msg = "You've already selected Stop once," + msg = msg + " would you like to 'Force Stop' the build?\n\n" + msg = msg + "This will stop the build as quickly as possible but may" + msg = msg + " well leave your build directory in an unusable state" + msg = msg + " that requires manual steps to fix." + dialog = CrumbsMessageDialog(self, lbl, gtk.MESSAGE_WARNING, msg) + button = dialog.add_button("Cancel", gtk.RESPONSE_CANCEL) + HobAltButton.style_button(button) + button = dialog.add_button("Force Stop", gtk.RESPONSE_YES) + HobButton.style_button(button) + else: + lbl = "Stop build?" + msg = "Are you sure you want to stop this" + msg = msg + " build?\n\n'Stop' will stop the build as soon as all in" + msg = msg + " progress build tasks are finished. However if a" + msg = msg + " lengthy compilation phase is in progress this may take" + msg = msg + " some time.\n\n" + msg = msg + "'Force Stop' will stop the build as quickly as" + msg = msg + " possible but may well leave your build directory in an" + msg = msg + " unusable state that requires manual steps to fix." + dialog = CrumbsMessageDialog(self, lbl, gtk.MESSAGE_WARNING, msg) + button = dialog.add_button("Cancel", gtk.RESPONSE_CANCEL) + HobAltButton.style_button(button) + button = dialog.add_button("Force stop", gtk.RESPONSE_YES) + HobAltButton.style_button(button) + button = dialog.add_button("Stop", gtk.RESPONSE_OK) + HobButton.style_button(button) + response = dialog.run() + dialog.destroy() + if response != gtk.RESPONSE_CANCEL: + self.stopping = True + if response == gtk.RESPONSE_OK: + self.build_details_page.progress_bar.set_stop_title("Stopping the build....") + self.build_details_page.progress_bar.set_rcstyle("stop") + self.cancel_build_sync() + elif response == gtk.RESPONSE_YES: + self.cancel_build_sync(True) + + def do_log(self, consolelogfile = None): + if consolelogfile: + bb.utils.mkdirhier(os.path.dirname(consolelogfile)) + if self.consolelog: + self.logger.removeHandler(self.consolelog) + self.consolelog = None + self.consolelog = logging.FileHandler(consolelogfile) + bb.msg.addDefaultlogFilter(self.consolelog) + format = bb.msg.BBLogFormatter("%(levelname)s: %(message)s") + self.consolelog.setFormatter(format) + + self.logger.addHandler(self.consolelog) + + def get_topdir(self): + return self.handler.get_topdir() + + def wait(self, delay): + time_start = time.time() + time_end = time_start + delay + while time_end > time.time(): + while gtk.events_pending(): + gtk.main_iteration() diff --git a/bitbake/lib/bb/ui/crumbs/buildmanager.py b/bitbake/lib/bb/ui/crumbs/buildmanager.py new file mode 100644 index 0000000000..e858d75e4c --- /dev/null +++ b/bitbake/lib/bb/ui/crumbs/buildmanager.py @@ -0,0 +1,455 @@ +# +# BitBake Graphical GTK User Interface +# +# Copyright (C) 2008 Intel Corporation +# +# Authored by Rob Bradford +# +# 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-- 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-- 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) diff --git a/bitbake/lib/bb/ui/crumbs/hig/__init__.py b/bitbake/lib/bb/ui/crumbs/hig/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/bitbake/lib/bb/ui/crumbs/hig/advancedsettingsdialog.py b/bitbake/lib/bb/ui/crumbs/hig/advancedsettingsdialog.py new file mode 100644 index 0000000000..e0b3553c2f --- /dev/null +++ b/bitbake/lib/bb/ui/crumbs/hig/advancedsettingsdialog.py @@ -0,0 +1,341 @@ +# +# BitBake Graphical GTK User Interface +# +# Copyright (C) 2011-2012 Intel Corporation +# +# Authored by Joshua Lock +# Authored by Dongxiao Xu +# Authored by Shane Wang +# +# 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 hashlib +from bb.ui.crumbs.hobwidget import HobInfoButton, HobButton +from bb.ui.crumbs.progressbar import HobProgressBar +from bb.ui.crumbs.hig.settingsuihelper import SettingsUIHelper +from bb.ui.crumbs.hig.crumbsdialog import CrumbsDialog +from bb.ui.crumbs.hig.crumbsmessagedialog import CrumbsMessageDialog +from bb.ui.crumbs.hig.proxydetailsdialog import ProxyDetailsDialog + +""" +The following are convenience classes for implementing GNOME HIG compliant +BitBake GUI's +In summary: spacing = 12px, border-width = 6px +""" + +class AdvancedSettingsDialog (CrumbsDialog, SettingsUIHelper): + + def details_cb(self, button, parent, protocol): + dialog = ProxyDetailsDialog(title = protocol.upper() + " Proxy Details", + user = self.configuration.proxies[protocol][1], + passwd = self.configuration.proxies[protocol][2], + parent = parent, + flags = gtk.DIALOG_MODAL + | gtk.DIALOG_DESTROY_WITH_PARENT + | gtk.DIALOG_NO_SEPARATOR) + dialog.add_button(gtk.STOCK_CLOSE, gtk.RESPONSE_OK) + response = dialog.run() + if response == gtk.RESPONSE_OK: + self.configuration.proxies[protocol][1] = dialog.user + self.configuration.proxies[protocol][2] = dialog.passwd + self.refresh_proxy_components() + dialog.destroy() + + def set_save_button(self, button): + self.save_button = button + + def rootfs_combo_changed_cb(self, rootfs_combo, all_package_format, check_hbox): + combo_item = self.rootfs_combo.get_active_text() + modified = False + for child in check_hbox.get_children(): + if isinstance(child, gtk.CheckButton): + check_hbox.remove(child) + modified = True + for format in all_package_format: + if format != combo_item: + check_button = gtk.CheckButton(format) + check_hbox.pack_start(check_button, expand=False, fill=False) + modified = True + if modified: + check_hbox.remove(self.pkgfmt_info) + check_hbox.pack_start(self.pkgfmt_info, expand=False, fill=False) + check_hbox.show_all() + + def gen_pkgfmt_widget(self, curr_package_format, all_package_format, tooltip_combo="", tooltip_extra=""): + pkgfmt_vbox = gtk.VBox(False, 6) + + label = self.gen_label_widget("Root file system package format") + pkgfmt_vbox.pack_start(label, expand=False, fill=False) + + rootfs_format = "" + if curr_package_format: + rootfs_format = curr_package_format.split()[0] + + rootfs_format_widget, rootfs_combo = self.gen_combo_widget(rootfs_format, all_package_format, tooltip_combo) + pkgfmt_vbox.pack_start(rootfs_format_widget, expand=False, fill=False) + + label = self.gen_label_widget("Additional package formats") + pkgfmt_vbox.pack_start(label, expand=False, fill=False) + + check_hbox = gtk.HBox(False, 12) + pkgfmt_vbox.pack_start(check_hbox, expand=False, fill=False) + for format in all_package_format: + if format != rootfs_format: + check_button = gtk.CheckButton(format) + is_active = (format in curr_package_format.split()) + check_button.set_active(is_active) + check_hbox.pack_start(check_button, expand=False, fill=False) + + self.pkgfmt_info = HobInfoButton(tooltip_extra, self) + check_hbox.pack_start(self.pkgfmt_info, expand=False, fill=False) + + rootfs_combo.connect("changed", self.rootfs_combo_changed_cb, all_package_format, check_hbox) + + pkgfmt_vbox.show_all() + + return pkgfmt_vbox, rootfs_combo, check_hbox + + def __init__(self, title, configuration, all_image_types, + all_package_formats, all_distros, all_sdk_machines, + max_threads, parent, flags, buttons=None): + super(AdvancedSettingsDialog, self).__init__(title, parent, flags, buttons) + + # class members from other objects + # bitbake settings from Builder.Configuration + self.configuration = configuration + self.image_types = all_image_types + self.all_package_formats = all_package_formats + self.all_distros = all_distros[:] + self.all_sdk_machines = all_sdk_machines + self.max_threads = max_threads + + # class members for internal use + self.distro_combo = None + self.dldir_text = None + self.sstatedir_text = None + self.sstatemirror_text = None + self.bb_spinner = None + self.pmake_spinner = None + self.rootfs_size_spinner = None + self.extra_size_spinner = None + self.gplv3_checkbox = None + self.sdk_checkbox = None + self.image_types_checkbuttons = {} + + self.md5 = self.config_md5() + self.settings_changed = False + + # create visual elements on the dialog + self.save_button = None + self.create_visual_elements() + self.connect("response", self.response_cb) + + def _get_sorted_value(self, var): + return " ".join(sorted(str(var).split())) + "\n" + + def config_md5(self): + data = "" + data += ("PACKAGE_CLASSES: " + self.configuration.curr_package_format + '\n') + data += ("DISTRO: " + self._get_sorted_value(self.configuration.curr_distro)) + data += ("IMAGE_ROOTFS_SIZE: " + self._get_sorted_value(self.configuration.image_rootfs_size)) + data += ("IMAGE_EXTRA_SIZE: " + self._get_sorted_value(self.configuration.image_extra_size)) + data += ("INCOMPATIBLE_LICENSE: " + self._get_sorted_value(self.configuration.incompat_license)) + data += ("SDK_MACHINE: " + self._get_sorted_value(self.configuration.curr_sdk_machine)) + data += ("TOOLCHAIN_BUILD: " + self._get_sorted_value(self.configuration.toolchain_build)) + data += ("IMAGE_FSTYPES: " + self._get_sorted_value(self.configuration.image_fstypes)) + return hashlib.md5(data).hexdigest() + + def create_visual_elements(self): + self.nb = gtk.Notebook() + self.nb.set_show_tabs(True) + self.nb.append_page(self.create_image_types_page(), gtk.Label("Image types")) + self.nb.append_page(self.create_output_page(), gtk.Label("Output")) + self.nb.set_current_page(0) + self.vbox.pack_start(self.nb, expand=True, fill=True) + self.vbox.pack_end(gtk.HSeparator(), expand=True, fill=True) + + self.show_all() + + def get_num_checked_image_types(self): + total = 0 + for b in self.image_types_checkbuttons.values(): + if b.get_active(): + total = total + 1 + return total + + def set_save_button_state(self): + if self.save_button: + self.save_button.set_sensitive(self.get_num_checked_image_types() > 0) + + def image_type_checkbutton_clicked_cb(self, button): + self.set_save_button_state() + if self.get_num_checked_image_types() == 0: + # Show an error dialog + lbl = "Select an image type" + msg = "You need to select at least one image type." + dialog = CrumbsMessageDialog(self, lbl, gtk.MESSAGE_WARNING, msg) + button = dialog.add_button("OK", gtk.RESPONSE_OK) + HobButton.style_button(button) + response = dialog.run() + dialog.destroy() + + def create_image_types_page(self): + main_vbox = gtk.VBox(False, 16) + main_vbox.set_border_width(6) + + advanced_vbox = gtk.VBox(False, 6) + advanced_vbox.set_border_width(6) + + distro_vbox = gtk.VBox(False, 6) + label = self.gen_label_widget("Distro:") + tooltip = "Selects the Yocto Project distribution you want" + try: + i = self.all_distros.index( "defaultsetup" ) + except ValueError: + i = -1 + if i != -1: + self.all_distros[ i ] = "Default" + if self.configuration.curr_distro == "defaultsetup": + self.configuration.curr_distro = "Default" + distro_widget, self.distro_combo = self.gen_combo_widget(self.configuration.curr_distro, self.all_distros,"Distro" + "*" + tooltip) + distro_vbox.pack_start(label, expand=False, fill=False) + distro_vbox.pack_start(distro_widget, expand=False, fill=False) + main_vbox.pack_start(distro_vbox, expand=False, fill=False) + + + rows = (len(self.image_types)+1)/3 + table = gtk.Table(rows + 1, 10, True) + advanced_vbox.pack_start(table, expand=False, fill=False) + + tooltip = "Image file system types you want." + info = HobInfoButton("Image types" + "*" + tooltip, self) + label = self.gen_label_widget("Image types:") + align = gtk.Alignment(0, 0.5, 0, 0) + table.attach(align, 0, 4, 0, 1) + align.add(label) + table.attach(info, 4, 5, 0, 1) + + i = 1 + j = 1 + for image_type in sorted(self.image_types): + self.image_types_checkbuttons[image_type] = gtk.CheckButton(image_type) + self.image_types_checkbuttons[image_type].connect("toggled", self.image_type_checkbutton_clicked_cb) + article = "" + if image_type.startswith(("a", "e", "i", "o", "u")): + article = "n" + if image_type == "live": + self.image_types_checkbuttons[image_type].set_tooltip_text("Build iso and hddimg images") + else: + self.image_types_checkbuttons[image_type].set_tooltip_text("Build a%s %s image" % (article, image_type)) + table.attach(self.image_types_checkbuttons[image_type], j - 1, j + 3, i, i + 1) + if image_type in self.configuration.image_fstypes.split(): + self.image_types_checkbuttons[image_type].set_active(True) + i += 1 + if i > rows: + i = 1 + j = j + 4 + + main_vbox.pack_start(advanced_vbox, expand=False, fill=False) + self.set_save_button_state() + + return main_vbox + + def create_output_page(self): + advanced_vbox = gtk.VBox(False, 6) + advanced_vbox.set_border_width(6) + + advanced_vbox.pack_start(self.gen_label_widget('Package format'), expand=False, fill=False) + sub_vbox = gtk.VBox(False, 6) + advanced_vbox.pack_start(sub_vbox, expand=False, fill=False) + tooltip_combo = "Selects the package format used to generate rootfs." + tooltip_extra = "Selects extra package formats to build" + pkgfmt_widget, self.rootfs_combo, self.check_hbox = self.gen_pkgfmt_widget(self.configuration.curr_package_format, self.all_package_formats,"Root file system package format" + "*" + tooltip_combo,"Additional package formats" + "*" + tooltip_extra) + sub_vbox.pack_start(pkgfmt_widget, expand=False, fill=False) + + advanced_vbox.pack_start(self.gen_label_widget('Image size'), expand=False, fill=False) + sub_vbox = gtk.VBox(False, 6) + advanced_vbox.pack_start(sub_vbox, expand=False, fill=False) + label = self.gen_label_widget("Image basic size (in MB)") + tooltip = "Defines the size for the generated image. The OpenEmbedded build system determines the final size for the generated image using an algorithm that takes into account the initial disk space used for the generated image, the Image basic size value, and the Additional free space value.\n\nFor more information, check the Yocto Project Reference Manual." + rootfs_size_widget, self.rootfs_size_spinner = self.gen_spinner_widget(int(self.configuration.image_rootfs_size*1.0/1024), 0, 65536,"Image basic size" + "*" + tooltip) + sub_vbox.pack_start(label, expand=False, fill=False) + sub_vbox.pack_start(rootfs_size_widget, expand=False, fill=False) + + sub_vbox = gtk.VBox(False, 6) + advanced_vbox.pack_start(sub_vbox, expand=False, fill=False) + label = self.gen_label_widget("Additional free space (in MB)") + tooltip = "Sets extra free disk space to be added to the generated image. Use this variable when you want to ensure that a specific amount of free disk space is available on a device after an image is installed and running." + extra_size_widget, self.extra_size_spinner = self.gen_spinner_widget(int(self.configuration.image_extra_size*1.0/1024), 0, 65536,"Additional free space" + "*" + tooltip) + sub_vbox.pack_start(label, expand=False, fill=False) + sub_vbox.pack_start(extra_size_widget, expand=False, fill=False) + + advanced_vbox.pack_start(self.gen_label_widget('Licensing'), expand=False, fill=False) + self.gplv3_checkbox = gtk.CheckButton("Exclude GPLv3 packages") + self.gplv3_checkbox.set_tooltip_text("Check this box to prevent GPLv3 packages from being included in your image") + if "GPLv3" in self.configuration.incompat_license.split(): + self.gplv3_checkbox.set_active(True) + else: + self.gplv3_checkbox.set_active(False) + advanced_vbox.pack_start(self.gplv3_checkbox, expand=False, fill=False) + + advanced_vbox.pack_start(self.gen_label_widget('SDK'), expand=False, fill=False) + sub_hbox = gtk.HBox(False, 6) + advanced_vbox.pack_start(sub_hbox, expand=False, fill=False) + self.sdk_checkbox = gtk.CheckButton("Populate SDK") + tooltip = "Check this box to generate an SDK tarball that consists of the cross-toolchain and a sysroot that contains development packages for your image." + self.sdk_checkbox.set_tooltip_text(tooltip) + self.sdk_checkbox.set_active(self.configuration.toolchain_build) + sub_hbox.pack_start(self.sdk_checkbox, expand=False, fill=False) + + tooltip = "Select the host platform for which you want to run the toolchain contained in the SDK tarball." + sdk_machine_widget, self.sdk_machine_combo = self.gen_combo_widget(self.configuration.curr_sdk_machine, self.all_sdk_machines,"Populate SDK" + "*" + tooltip) + sub_hbox.pack_start(sdk_machine_widget, expand=False, fill=False) + + return advanced_vbox + + def response_cb(self, dialog, response_id): + package_format = [] + package_format.append(self.rootfs_combo.get_active_text()) + for child in self.check_hbox: + if isinstance(child, gtk.CheckButton) and child.get_active(): + package_format.append(child.get_label()) + self.configuration.curr_package_format = " ".join(package_format) + + distro = self.distro_combo.get_active_text() + if distro == "Default": + distro = "defaultsetup" + self.configuration.curr_distro = distro + self.configuration.image_rootfs_size = self.rootfs_size_spinner.get_value_as_int() * 1024 + self.configuration.image_extra_size = self.extra_size_spinner.get_value_as_int() * 1024 + + self.configuration.image_fstypes = "" + for image_type in self.image_types: + if self.image_types_checkbuttons[image_type].get_active(): + self.configuration.image_fstypes += (" " + image_type) + self.configuration.image_fstypes.strip() + + if self.gplv3_checkbox.get_active(): + if "GPLv3" not in self.configuration.incompat_license.split(): + self.configuration.incompat_license += " GPLv3" + else: + if "GPLv3" in self.configuration.incompat_license.split(): + self.configuration.incompat_license = self.configuration.incompat_license.split().remove("GPLv3") + self.configuration.incompat_license = " ".join(self.configuration.incompat_license or []) + self.configuration.incompat_license = self.configuration.incompat_license.strip() + + self.configuration.toolchain_build = self.sdk_checkbox.get_active() + self.configuration.curr_sdk_machine = self.sdk_machine_combo.get_active_text() + md5 = self.config_md5() + self.settings_changed = (self.md5 != md5) diff --git a/bitbake/lib/bb/ui/crumbs/hig/crumbsdialog.py b/bitbake/lib/bb/ui/crumbs/hig/crumbsdialog.py new file mode 100644 index 0000000000..c679f9a070 --- /dev/null +++ b/bitbake/lib/bb/ui/crumbs/hig/crumbsdialog.py @@ -0,0 +1,44 @@ +# +# BitBake Graphical GTK User Interface +# +# Copyright (C) 2011-2012 Intel Corporation +# +# Authored by Joshua Lock +# Authored by Dongxiao Xu +# Authored by Shane Wang +# +# 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 + +""" +The following are convenience classes for implementing GNOME HIG compliant +BitBake GUI's +In summary: spacing = 12px, border-width = 6px +""" + +class CrumbsDialog(gtk.Dialog): + """ + A GNOME HIG compliant dialog widget. + Add buttons with gtk.Dialog.add_button or gtk.Dialog.add_buttons + """ + def __init__(self, title="", parent=None, flags=0, buttons=None): + super(CrumbsDialog, self).__init__(title, parent, flags, buttons) + + self.set_property("has-separator", False) # note: deprecated in 2.22 + + self.set_border_width(6) + self.vbox.set_property("spacing", 12) + self.action_area.set_property("spacing", 12) + self.action_area.set_property("border-width", 6) diff --git a/bitbake/lib/bb/ui/crumbs/hig/crumbsmessagedialog.py b/bitbake/lib/bb/ui/crumbs/hig/crumbsmessagedialog.py new file mode 100644 index 0000000000..3b998e4637 --- /dev/null +++ b/bitbake/lib/bb/ui/crumbs/hig/crumbsmessagedialog.py @@ -0,0 +1,70 @@ +# +# BitBake Graphical GTK User Interface +# +# Copyright (C) 2011-2012 Intel Corporation +# +# Authored by Joshua Lock +# Authored by Dongxiao Xu +# Authored by Shane Wang +# +# 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 glib +import gtk +from bb.ui.crumbs.hobwidget import HobIconChecker +from bb.ui.crumbs.hig.crumbsdialog import CrumbsDialog + +""" +The following are convenience classes for implementing GNOME HIG compliant +BitBake GUI's +In summary: spacing = 12px, border-width = 6px +""" + +class CrumbsMessageDialog(gtk.MessageDialog): + """ + A GNOME HIG compliant dialog widget. + Add buttons with gtk.Dialog.add_button or gtk.Dialog.add_buttons + """ + def __init__(self, parent = None, label="", dialog_type = gtk.MESSAGE_QUESTION, msg=""): + super(CrumbsMessageDialog, self).__init__(None, + gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT, + dialog_type, + gtk.BUTTONS_NONE, + None) + + self.set_skip_taskbar_hint(False) + + self.set_markup(label) + + if 0 <= len(msg) < 300: + self.format_secondary_markup(msg) + else: + vbox = self.get_message_area() + vbox.set_border_width(1) + vbox.set_property("spacing", 12) + self.textWindow = gtk.ScrolledWindow() + self.textWindow.set_shadow_type(gtk.SHADOW_IN) + self.textWindow.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) + self.msgView = gtk.TextView() + self.msgView.set_editable(False) + self.msgView.set_wrap_mode(gtk.WRAP_WORD) + self.msgView.set_cursor_visible(False) + self.msgView.set_size_request(300, 300) + self.buf = gtk.TextBuffer() + self.buf.set_text(msg) + self.msgView.set_buffer(self.buf) + self.textWindow.add(self.msgView) + self.msgView.show() + vbox.add(self.textWindow) + self.textWindow.show() diff --git a/bitbake/lib/bb/ui/crumbs/hig/deployimagedialog.py b/bitbake/lib/bb/ui/crumbs/hig/deployimagedialog.py new file mode 100644 index 0000000000..a13fff906a --- /dev/null +++ b/bitbake/lib/bb/ui/crumbs/hig/deployimagedialog.py @@ -0,0 +1,219 @@ +# +# BitBake Graphical GTK User Interface +# +# Copyright (C) 2011-2012 Intel Corporation +# +# Authored by Joshua Lock +# Authored by Dongxiao Xu +# Authored by Shane Wang +# +# 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 glob +import gtk +import gobject +import os +import re +import shlex +import subprocess +import tempfile +from bb.ui.crumbs.hobwidget import hic, HobButton +from bb.ui.crumbs.progressbar import HobProgressBar +import bb.ui.crumbs.utils +import bb.process +from bb.ui.crumbs.hig.crumbsdialog import CrumbsDialog +from bb.ui.crumbs.hig.crumbsmessagedialog import CrumbsMessageDialog + +""" +The following are convenience classes for implementing GNOME HIG compliant +BitBake GUI's +In summary: spacing = 12px, border-width = 6px +""" + +class DeployImageDialog (CrumbsDialog): + + __dummy_usb__ = "--select a usb drive--" + + def __init__(self, title, image_path, parent, flags, buttons=None, standalone=False): + super(DeployImageDialog, self).__init__(title, parent, flags, buttons) + + self.image_path = image_path + self.standalone = standalone + + self.create_visual_elements() + self.connect("response", self.response_cb) + + def create_visual_elements(self): + self.set_size_request(600, 400) + label = gtk.Label() + label.set_alignment(0.0, 0.5) + markup = "The image to be written into usb drive:" + label.set_markup(markup) + self.vbox.pack_start(label, expand=False, fill=False, padding=2) + + table = gtk.Table(2, 10, False) + table.set_col_spacings(5) + table.set_row_spacings(5) + self.vbox.pack_start(table, expand=True, fill=True) + + scroll = gtk.ScrolledWindow() + scroll.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC) + scroll.set_shadow_type(gtk.SHADOW_IN) + tv = gtk.TextView() + tv.set_editable(False) + tv.set_wrap_mode(gtk.WRAP_WORD) + tv.set_cursor_visible(False) + self.buf = gtk.TextBuffer() + self.buf.set_text(self.image_path) + tv.set_buffer(self.buf) + scroll.add(tv) + table.attach(scroll, 0, 10, 0, 1) + + # There are 2 ways to use DeployImageDialog + # One way is that called by HOB when the 'Deploy Image' button is clicked + # The other way is that called by a standalone script. + # Following block of codes handles the latter way. It adds a 'Select Image' button and + # emit a signal when the button is clicked. + if self.standalone: + gobject.signal_new("select_image_clicked", self, gobject.SIGNAL_RUN_FIRST, + gobject.TYPE_NONE, ()) + icon = gtk.Image() + pix_buffer = gtk.gdk.pixbuf_new_from_file(hic.ICON_IMAGES_DISPLAY_FILE) + icon.set_from_pixbuf(pix_buffer) + button = gtk.Button("Select Image") + button.set_image(icon) + #button.set_size_request(140, 50) + table.attach(button, 9, 10, 1, 2, gtk.FILL, 0, 0, 0) + button.connect("clicked", self.select_image_button_clicked_cb) + + separator = gtk.HSeparator() + self.vbox.pack_start(separator, expand=False, fill=False, padding=10) + + self.usb_desc = gtk.Label() + self.usb_desc.set_alignment(0.0, 0.5) + markup = "You haven't chosen any USB drive." + self.usb_desc.set_markup(markup) + + self.usb_combo = gtk.combo_box_new_text() + self.usb_combo.connect("changed", self.usb_combo_changed_cb) + model = self.usb_combo.get_model() + model.clear() + self.usb_combo.append_text(self.__dummy_usb__) + for usb in self.find_all_usb_devices(): + self.usb_combo.append_text("/dev/" + usb) + self.usb_combo.set_active(0) + self.vbox.pack_start(self.usb_combo, expand=False, fill=False) + self.vbox.pack_start(self.usb_desc, expand=False, fill=False, padding=2) + + self.progress_bar = HobProgressBar() + self.vbox.pack_start(self.progress_bar, expand=False, fill=False) + separator = gtk.HSeparator() + self.vbox.pack_start(separator, expand=False, fill=True, padding=10) + + self.vbox.show_all() + self.progress_bar.hide() + + def set_image_text_buffer(self, image_path): + self.buf.set_text(image_path) + + def set_image_path(self, image_path): + self.image_path = image_path + + def popen_read(self, cmd): + tmpout, errors = bb.process.run("%s" % cmd) + return tmpout.strip() + + def find_all_usb_devices(self): + usb_devs = [ os.readlink(u) + for u in glob.glob('/dev/disk/by-id/usb*') + if not re.search(r'part\d+', u) ] + return [ '%s' % u[u.rfind('/')+1:] for u in usb_devs ] + + def get_usb_info(self, dev): + return "%s %s" % \ + (self.popen_read('cat /sys/class/block/%s/device/vendor' % dev), + self.popen_read('cat /sys/class/block/%s/device/model' % dev)) + + def select_image_button_clicked_cb(self, button): + self.emit('select_image_clicked') + + def usb_combo_changed_cb(self, usb_combo): + combo_item = self.usb_combo.get_active_text() + if not combo_item or combo_item == self.__dummy_usb__: + markup = "You haven't chosen any USB drive." + self.usb_desc.set_markup(markup) + else: + markup = "" + self.get_usb_info(combo_item.lstrip("/dev/")) + "" + self.usb_desc.set_markup(markup) + + def response_cb(self, dialog, response_id): + if response_id == gtk.RESPONSE_YES: + lbl = '' + msg = '' + combo_item = self.usb_combo.get_active_text() + if combo_item and combo_item != self.__dummy_usb__ and self.image_path: + cmdline = bb.ui.crumbs.utils.which_terminal() + if cmdline: + tmpfile = tempfile.NamedTemporaryFile() + cmdline += "\"sudo dd if=" + self.image_path + \ + " of=" + combo_item + " && sync; echo $? > " + tmpfile.name + "\"" + subprocess.call(shlex.split(cmdline)) + + if int(tmpfile.readline().strip()) == 0: + lbl = "Deploy image successfully." + else: + lbl = "Failed to deploy image." + msg = "Please check image %s exists and USB device %s is writable." % (self.image_path, combo_item) + tmpfile.close() + else: + if not self.image_path: + lbl = "No selection made." + msg = "You have not selected an image to deploy." + else: + lbl = "No selection made." + msg = "You have not selected a USB device." + if len(lbl): + crumbs_dialog = CrumbsMessageDialog(self, lbl, gtk.MESSAGE_INFO, msg) + button = crumbs_dialog.add_button("Close", gtk.RESPONSE_OK) + HobButton.style_button(button) + crumbs_dialog.run() + crumbs_dialog.destroy() + + def update_progress_bar(self, title, fraction, status=None): + self.progress_bar.update(fraction) + self.progress_bar.set_title(title) + self.progress_bar.set_rcstyle(status) + + def write_file(self, ifile, ofile): + self.progress_bar.reset() + self.progress_bar.show() + + f_from = os.open(ifile, os.O_RDONLY) + f_to = os.open(ofile, os.O_WRONLY) + + total_size = os.stat(ifile).st_size + written_size = 0 + + while True: + buf = os.read(f_from, 1024*1024) + if not buf: + break + os.write(f_to, buf) + written_size += 1024*1024 + self.update_progress_bar("Writing to usb:", written_size * 1.0/total_size) + + self.update_progress_bar("Writing completed:", 1.0) + os.close(f_from) + os.close(f_to) + self.progress_bar.hide() diff --git a/bitbake/lib/bb/ui/crumbs/hig/imageselectiondialog.py b/bitbake/lib/bb/ui/crumbs/hig/imageselectiondialog.py new file mode 100644 index 0000000000..21216adc97 --- /dev/null +++ b/bitbake/lib/bb/ui/crumbs/hig/imageselectiondialog.py @@ -0,0 +1,172 @@ +# +# BitBake Graphical GTK User Interface +# +# Copyright (C) 2011-2012 Intel Corporation +# +# Authored by Joshua Lock +# Authored by Dongxiao Xu +# Authored by Shane Wang +# +# 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 os +from bb.ui.crumbs.hobwidget import HobViewTable, HobInfoButton, HobButton, HobAltButton +from bb.ui.crumbs.hig.crumbsdialog import CrumbsDialog +from bb.ui.crumbs.hig.layerselectiondialog import LayerSelectionDialog + +""" +The following are convenience classes for implementing GNOME HIG compliant +BitBake GUI's +In summary: spacing = 12px, border-width = 6px +""" + +class ImageSelectionDialog (CrumbsDialog): + + __columns__ = [{ + 'col_name' : 'Image name', + 'col_id' : 0, + 'col_style': 'text', + 'col_min' : 400, + 'col_max' : 400 + }, { + 'col_name' : 'Select', + 'col_id' : 1, + 'col_style': 'radio toggle', + 'col_min' : 160, + 'col_max' : 160 + }] + + + def __init__(self, image_folder, image_types, title, parent, flags, buttons=None, image_extension = {}): + super(ImageSelectionDialog, self).__init__(title, parent, flags, buttons) + self.connect("response", self.response_cb) + + self.image_folder = image_folder + self.image_types = image_types + self.image_list = [] + self.image_names = [] + self.image_extension = image_extension + + # create visual elements on the dialog + self.create_visual_elements() + + self.image_store = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_BOOLEAN) + self.fill_image_store() + + def create_visual_elements(self): + hbox = gtk.HBox(False, 6) + + self.vbox.pack_start(hbox, expand=False, fill=False) + + entry = gtk.Entry() + entry.set_text(self.image_folder) + table = gtk.Table(1, 10, True) + table.set_size_request(560, -1) + hbox.pack_start(table, expand=False, fill=False) + table.attach(entry, 0, 9, 0, 1) + image = gtk.Image() + image.set_from_stock(gtk.STOCK_OPEN, gtk.ICON_SIZE_BUTTON) + open_button = gtk.Button() + open_button.set_image(image) + open_button.connect("clicked", self.select_path_cb, self, entry) + table.attach(open_button, 9, 10, 0, 1) + + self.image_table = HobViewTable(self.__columns__, "Images") + self.image_table.set_size_request(-1, 300) + self.image_table.connect("toggled", self.toggled_cb) + self.image_table.connect_group_selection(self.table_selected_cb) + self.image_table.connect("row-activated", self.row_actived_cb) + self.vbox.pack_start(self.image_table, expand=True, fill=True) + + self.show_all() + + def change_image_cb(self, model, path, columnid): + if not model: + return + iter = model.get_iter_first() + while iter: + rowpath = model.get_path(iter) + model[rowpath][columnid] = False + iter = model.iter_next(iter) + + model[path][columnid] = True + + def toggled_cb(self, table, cell, path, columnid, tree): + model = tree.get_model() + self.change_image_cb(model, path, columnid) + + def table_selected_cb(self, selection): + model, paths = selection.get_selected_rows() + if paths: + self.change_image_cb(model, paths[0], 1) + + def row_actived_cb(self, tab, model, path): + self.change_image_cb(model, path, 1) + self.emit('response', gtk.RESPONSE_YES) + + def select_path_cb(self, action, parent, entry): + dialog = gtk.FileChooserDialog("", parent, + gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER) + text = entry.get_text() + dialog.set_current_folder(text if len(text) > 0 else os.getcwd()) + button = dialog.add_button("Cancel", gtk.RESPONSE_NO) + HobAltButton.style_button(button) + button = dialog.add_button("Open", gtk.RESPONSE_YES) + HobButton.style_button(button) + response = dialog.run() + if response == gtk.RESPONSE_YES: + path = dialog.get_filename() + entry.set_text(path) + self.image_folder = path + self.fill_image_store() + + dialog.destroy() + + def fill_image_store(self): + self.image_list = [] + self.image_store.clear() + imageset = set() + for root, dirs, files in os.walk(self.image_folder): + # ignore the sub directories + dirs[:] = [] + for f in files: + for image_type in self.image_types: + if image_type in self.image_extension: + real_types = self.image_extension[image_type] + else: + real_types = [image_type] + for real_image_type in real_types: + if f.endswith('.' + real_image_type): + imageset.add(f.rsplit('.' + real_image_type)[0].rsplit('.rootfs')[0]) + self.image_list.append(f) + + for image in imageset: + self.image_store.set(self.image_store.append(), 0, image, 1, False) + + self.image_table.set_model(self.image_store) + + def response_cb(self, dialog, response_id): + self.image_names = [] + if response_id == gtk.RESPONSE_YES: + iter = self.image_store.get_iter_first() + while iter: + path = self.image_store.get_path(iter) + if self.image_store[path][1]: + for f in self.image_list: + if f.startswith(self.image_store[path][0] + '.'): + self.image_names.append(f) + break + iter = self.image_store.iter_next(iter) diff --git a/bitbake/lib/bb/ui/crumbs/hig/layerselectiondialog.py b/bitbake/lib/bb/ui/crumbs/hig/layerselectiondialog.py new file mode 100644 index 0000000000..52d57b6738 --- /dev/null +++ b/bitbake/lib/bb/ui/crumbs/hig/layerselectiondialog.py @@ -0,0 +1,298 @@ +# +# BitBake Graphical GTK User Interface +# +# Copyright (C) 2011-2012 Intel Corporation +# +# Authored by Joshua Lock +# Authored by Dongxiao Xu +# Authored by Shane Wang +# +# 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 os +import tempfile +from bb.ui.crumbs.hobwidget import hic, HobButton, HobAltButton +from bb.ui.crumbs.hig.crumbsdialog import CrumbsDialog +from bb.ui.crumbs.hig.crumbsmessagedialog import CrumbsMessageDialog + +""" +The following are convenience classes for implementing GNOME HIG compliant +BitBake GUI's +In summary: spacing = 12px, border-width = 6px +""" + +class CellRendererPixbufActivatable(gtk.CellRendererPixbuf): + """ + A custom CellRenderer implementation which is activatable + so that we can handle user clicks + """ + __gsignals__ = { 'clicked' : (gobject.SIGNAL_RUN_LAST, + gobject.TYPE_NONE, + (gobject.TYPE_STRING,)), } + + def __init__(self): + gtk.CellRendererPixbuf.__init__(self) + self.set_property('mode', gtk.CELL_RENDERER_MODE_ACTIVATABLE) + self.set_property('follow-state', True) + + """ + Respond to a user click on a cell + """ + def do_activate(self, even, widget, path, background_area, cell_area, flags): + self.emit('clicked', path) + +# +# LayerSelectionDialog +# +class LayerSelectionDialog (CrumbsDialog): + + TARGETS = [ + ("MY_TREE_MODEL_ROW", gtk.TARGET_SAME_WIDGET, 0), + ("text/plain", 0, 1), + ("TEXT", 0, 2), + ("STRING", 0, 3), + ] + + def gen_label_widget(self, content): + label = gtk.Label() + label.set_alignment(0, 0) + label.set_markup(content) + label.show() + return label + + def layer_widget_toggled_cb(self, cell, path, layer_store): + name = layer_store[path][0] + toggle = not layer_store[path][1] + layer_store[path][1] = toggle + + def layer_widget_add_clicked_cb(self, action, layer_store, parent): + dialog = gtk.FileChooserDialog("Add new layer", parent, + gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER) + button = dialog.add_button("Cancel", gtk.RESPONSE_NO) + HobAltButton.style_button(button) + button = dialog.add_button("Open", gtk.RESPONSE_YES) + HobButton.style_button(button) + label = gtk.Label("Select the layer you wish to add") + label.show() + dialog.set_extra_widget(label) + response = dialog.run() + path = dialog.get_filename() + dialog.destroy() + + lbl = "Error" + msg = "Unable to load layer %s because " % path + if response == gtk.RESPONSE_YES: + import os + import os.path + layers = [] + it = layer_store.get_iter_first() + while it: + layers.append(layer_store.get_value(it, 0)) + it = layer_store.iter_next(it) + + if not path: + msg += "it is an invalid path." + elif not os.path.exists(path+"/conf/layer.conf"): + msg += "there is no layer.conf inside the directory." + elif path in layers: + msg += "it is already in loaded layers." + else: + layer_store.append([path]) + return + dialog = CrumbsMessageDialog(parent, lbl, gtk.MESSAGE_ERROR, msg) + dialog.add_button(gtk.STOCK_CLOSE, gtk.RESPONSE_OK) + response = dialog.run() + dialog.destroy() + + def layer_widget_del_clicked_cb(self, action, tree_selection, layer_store): + model, iter = tree_selection.get_selected() + if iter: + layer_store.remove(iter) + + + def gen_layer_widget(self, layers, layers_avail, window, tooltip=""): + hbox = gtk.HBox(False, 6) + + layer_tv = gtk.TreeView() + layer_tv.set_rules_hint(True) + layer_tv.set_headers_visible(False) + tree_selection = layer_tv.get_selection() + tree_selection.set_mode(gtk.SELECTION_SINGLE) + + # Allow enable drag and drop of rows including row move + dnd_internal_target = '' + dnd_targets = [(dnd_internal_target, gtk.TARGET_SAME_WIDGET, 0)] + layer_tv.enable_model_drag_source( gtk.gdk.BUTTON1_MASK, + dnd_targets, + gtk.gdk.ACTION_MOVE) + layer_tv.enable_model_drag_dest(dnd_targets, + gtk.gdk.ACTION_MOVE) + layer_tv.connect("drag_data_get", self.drag_data_get_cb) + layer_tv.connect("drag_data_received", self.drag_data_received_cb) + + col0= gtk.TreeViewColumn('Path') + cell0 = gtk.CellRendererText() + cell0.set_padding(5,2) + col0.pack_start(cell0, True) + col0.set_cell_data_func(cell0, self.draw_layer_path_cb) + layer_tv.append_column(col0) + + scroll = gtk.ScrolledWindow() + scroll.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC) + scroll.set_shadow_type(gtk.SHADOW_IN) + scroll.add(layer_tv) + + table_layer = gtk.Table(2, 10, False) + hbox.pack_start(table_layer, expand=True, fill=True) + + table_layer.attach(scroll, 0, 10, 0, 1) + + layer_store = gtk.ListStore(gobject.TYPE_STRING) + for layer in layers: + layer_store.append([layer]) + + col1 = gtk.TreeViewColumn('Enabled') + layer_tv.append_column(col1) + + cell1 = CellRendererPixbufActivatable() + cell1.set_fixed_size(-1,35) + cell1.connect("clicked", self.del_cell_clicked_cb, layer_store) + col1.pack_start(cell1, True) + col1.set_cell_data_func(cell1, self.draw_delete_button_cb, layer_tv) + + add_button = gtk.Button() + add_button.set_relief(gtk.RELIEF_NONE) + box = gtk.HBox(False, 6) + box.show() + add_button.add(box) + add_button.connect("enter-notify-event", self.add_hover_cb) + add_button.connect("leave-notify-event", self.add_leave_cb) + self.im = gtk.Image() + self.im.set_from_file(hic.ICON_INDI_ADD_FILE) + self.im.show() + box.pack_start(self.im, expand=False, fill=False, padding=6) + lbl = gtk.Label("Add layer") + lbl.set_alignment(0.0, 0.5) + lbl.show() + box.pack_start(lbl, expand=True, fill=True, padding=6) + add_button.connect("clicked", self.layer_widget_add_clicked_cb, layer_store, window) + table_layer.attach(add_button, 0, 10, 1, 2, gtk.EXPAND | gtk.FILL, 0, 0, 6) + layer_tv.set_model(layer_store) + + hbox.show_all() + + return hbox, layer_store + + def drag_data_get_cb(self, treeview, context, selection, target_id, etime): + treeselection = treeview.get_selection() + model, iter = treeselection.get_selected() + data = model.get_value(iter, 0) + selection.set(selection.target, 8, data) + + def drag_data_received_cb(self, treeview, context, x, y, selection, info, etime): + model = treeview.get_model() + data = selection.data + drop_info = treeview.get_dest_row_at_pos(x, y) + if drop_info: + path, position = drop_info + iter = model.get_iter(path) + if (position == gtk.TREE_VIEW_DROP_BEFORE or position == gtk.TREE_VIEW_DROP_INTO_OR_BEFORE): + model.insert_before(iter, [data]) + else: + model.insert_after(iter, [data]) + else: + model.append([data]) + if context.action == gtk.gdk.ACTION_MOVE: + context.finish(True, True, etime) + return + + def add_hover_cb(self, button, event): + self.im.set_from_file(hic.ICON_INDI_ADD_HOVER_FILE) + + def add_leave_cb(self, button, event): + self.im.set_from_file(hic.ICON_INDI_ADD_FILE) + + def __init__(self, title, layers, layers_non_removable, all_layers, parent, flags, buttons=None): + super(LayerSelectionDialog, self).__init__(title, parent, flags, buttons) + + # class members from other objects + self.layers = layers + self.layers_non_removable = layers_non_removable + self.all_layers = all_layers + self.layers_changed = False + + # icon for remove button in TreeView + im = gtk.Image() + im.set_from_file(hic.ICON_INDI_REMOVE_FILE) + self.rem_icon = im.get_pixbuf() + + # class members for internal use + self.layer_store = None + + # create visual elements on the dialog + self.create_visual_elements() + self.connect("response", self.response_cb) + + def create_visual_elements(self): + layer_widget, self.layer_store = self.gen_layer_widget(self.layers, self.all_layers, self, None) + layer_widget.set_size_request(450, 250) + self.vbox.pack_start(layer_widget, expand=True, fill=True) + self.show_all() + + def response_cb(self, dialog, response_id): + model = self.layer_store + it = model.get_iter_first() + layers = [] + while it: + layers.append(model.get_value(it, 0)) + it = model.iter_next(it) + + self.layers_changed = (self.layers != layers) + self.layers = layers + + """ + A custom cell_data_func to draw a delete 'button' in the TreeView for layers + other than the meta layer. The deletion of which is prevented so that the + user can't shoot themselves in the foot too badly. + """ + def draw_delete_button_cb(self, col, cell, model, it, tv): + path = model.get_value(it, 0) + if path in self.layers_non_removable: + cell.set_sensitive(False) + cell.set_property('pixbuf', None) + cell.set_property('mode', gtk.CELL_RENDERER_MODE_INERT) + else: + cell.set_property('pixbuf', self.rem_icon) + cell.set_sensitive(True) + cell.set_property('mode', gtk.CELL_RENDERER_MODE_ACTIVATABLE) + + return True + + """ + A custom cell_data_func to write an extra message into the layer path cell + for the meta layer. We should inform the user that they can't remove it for + their own safety. + """ + def draw_layer_path_cb(self, col, cell, model, it): + path = model.get_value(it, 0) + if path in self.layers_non_removable: + cell.set_property('markup', "It cannot be removed\n%s" % path) + else: + cell.set_property('text', path) + + def del_cell_clicked_cb(self, cell, path, model): + it = model.get_iter_from_string(path) + model.remove(it) diff --git a/bitbake/lib/bb/ui/crumbs/hig/parsingwarningsdialog.py b/bitbake/lib/bb/ui/crumbs/hig/parsingwarningsdialog.py new file mode 100644 index 0000000000..33bac39db8 --- /dev/null +++ b/bitbake/lib/bb/ui/crumbs/hig/parsingwarningsdialog.py @@ -0,0 +1,163 @@ +# +# BitBake Graphical GTK User Interface +# +# Copyright (C) 2011-2012 Intel Corporation +# +# Authored by Cristiana Voicu +# +# 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 +from bb.ui.crumbs.hobwidget import HobAltButton +from bb.ui.crumbs.hig.crumbsdialog import CrumbsDialog + +""" +The following are convenience classes for implementing GNOME HIG compliant +BitBake GUI's +In summary: spacing = 12px, border-width = 6px +""" + +# +# ParsingWarningsDialog +# +class ParsingWarningsDialog (CrumbsDialog): + + def __init__(self, title, warnings, parent, flags, buttons=None): + super(ParsingWarningsDialog, self).__init__(title, parent, flags, buttons) + + self.warnings = warnings + self.warning_on = 0 + self.warn_nb = len(warnings) + + # create visual elements on the dialog + self.create_visual_elements() + + def cancel_button_cb(self, button): + self.destroy() + + def previous_button_cb(self, button): + self.warning_on = self.warning_on - 1 + self.refresh_components() + + def next_button_cb(self, button): + self.warning_on = self.warning_on + 1 + self.refresh_components() + + def refresh_components(self): + lbl = self.warnings[self.warning_on] + #when the warning text has more than 400 chars, it uses a scroll bar + if 0<= len(lbl) < 400: + self.warning_label.set_size_request(320, 230) + self.warning_label.set_use_markup(True) + self.warning_label.set_line_wrap(True) + self.warning_label.set_markup(lbl) + self.warning_label.set_property("yalign", 0.00) + else: + self.textWindow.set_shadow_type(gtk.SHADOW_IN) + self.textWindow.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) + self.msgView = gtk.TextView() + self.msgView.set_editable(False) + self.msgView.set_wrap_mode(gtk.WRAP_WORD) + self.msgView.set_cursor_visible(False) + self.msgView.set_size_request(320, 230) + self.buf = gtk.TextBuffer() + self.buf.set_text(lbl) + self.msgView.set_buffer(self.buf) + self.textWindow.add(self.msgView) + self.msgView.show() + + if self.warning_on==0: + self.previous_button.set_sensitive(False) + else: + self.previous_button.set_sensitive(True) + + if self.warning_on==self.warn_nb-1: + self.next_button.set_sensitive(False) + else: + self.next_button.set_sensitive(True) + + if self.warn_nb>1: + self.heading = "Warning " + str(self.warning_on + 1) + " of " + str(self.warn_nb) + self.heading_label.set_markup('%s' % self.heading) + else: + self.heading = "Warning" + self.heading_label.set_markup('%s' % self.heading) + + self.show_all() + + if 0<= len(lbl) < 400: + self.textWindow.hide() + else: + self.warning_label.hide() + + def create_visual_elements(self): + self.set_size_request(350, 350) + self.heading_label = gtk.Label() + self.heading_label.set_alignment(0, 0) + self.warning_label = gtk.Label() + self.warning_label.set_selectable(True) + self.warning_label.set_alignment(0, 0) + self.textWindow = gtk.ScrolledWindow() + + table = gtk.Table(1, 10, False) + + cancel_button = gtk.Button() + cancel_button.set_label("Close") + cancel_button.connect("clicked", self.cancel_button_cb) + cancel_button.set_size_request(110, 30) + + self.previous_button = gtk.Button() + image1 = gtk.image_new_from_stock(gtk.STOCK_GO_BACK, gtk.ICON_SIZE_BUTTON) + image1.show() + box = gtk.HBox(False, 6) + box.show() + self.previous_button.add(box) + lbl = gtk.Label("Previous") + lbl.show() + box.pack_start(image1, expand=False, fill=False, padding=3) + box.pack_start(lbl, expand=True, fill=True, padding=3) + self.previous_button.connect("clicked", self.previous_button_cb) + self.previous_button.set_size_request(110, 30) + + self.next_button = gtk.Button() + image2 = gtk.image_new_from_stock(gtk.STOCK_GO_FORWARD, gtk.ICON_SIZE_BUTTON) + image2.show() + box = gtk.HBox(False, 6) + box.show() + self.next_button.add(box) + lbl = gtk.Label("Next") + lbl.show() + box.pack_start(lbl, expand=True, fill=True, padding=3) + box.pack_start(image2, expand=False, fill=False, padding=3) + self.next_button.connect("clicked", self.next_button_cb) + self.next_button.set_size_request(110, 30) + + #when there more than one warning, we need "previous" and "next" button + if self.warn_nb>1: + self.vbox.pack_start(self.heading_label, expand=False, fill=False) + self.vbox.pack_start(self.warning_label, expand=False, fill=False) + self.vbox.pack_start(self.textWindow, expand=False, fill=False) + table.attach(cancel_button, 6, 7, 0, 1, xoptions=gtk.SHRINK) + table.attach(self.previous_button, 7, 8, 0, 1, xoptions=gtk.SHRINK) + table.attach(self.next_button, 8, 9, 0, 1, xoptions=gtk.SHRINK) + self.vbox.pack_end(table, expand=False, fill=False) + else: + self.vbox.pack_start(self.heading_label, expand=False, fill=False) + self.vbox.pack_start(self.warning_label, expand=False, fill=False) + self.vbox.pack_start(self.textWindow, expand=False, fill=False) + cancel_button = self.add_button("Close", gtk.RESPONSE_CANCEL) + HobAltButton.style_button(cancel_button) + + self.refresh_components() diff --git a/bitbake/lib/bb/ui/crumbs/hig/propertydialog.py b/bitbake/lib/bb/ui/crumbs/hig/propertydialog.py new file mode 100644 index 0000000000..09b9ce6de3 --- /dev/null +++ b/bitbake/lib/bb/ui/crumbs/hig/propertydialog.py @@ -0,0 +1,437 @@ +# +# BitBake Graphical GTK User Interface +# +# Copyright (C) 2011-2013 Intel Corporation +# +# Authored by Andrei Dinu +# +# 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 string +import gtk +import gobject +import os +import tempfile +import glib +from bb.ui.crumbs.hig.crumbsdialog import CrumbsDialog +from bb.ui.crumbs.hig.settingsuihelper import SettingsUIHelper +from bb.ui.crumbs.hig.crumbsmessagedialog import CrumbsMessageDialog +from bb.ui.crumbs.hig.layerselectiondialog import LayerSelectionDialog + +""" +The following are convenience classes for implementing GNOME HIG compliant +BitBake GUI's +In summary: spacing = 12px, border-width = 6px +""" + +class PropertyDialog(CrumbsDialog): + + def __init__(self, title, parent, information, flags, buttons=None): + + super(PropertyDialog, self).__init__(title, parent, flags, buttons) + + self.properties = information + + if len(self.properties) == 10: + self.create_recipe_visual_elements() + elif len(self.properties) == 5: + self.create_package_visual_elements() + else: + self.create_information_visual_elements() + + + def create_information_visual_elements(self): + + HOB_ICON_BASE_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), ("icons/")) + ICON_PACKAGES_DISPLAY_FILE = os.path.join(HOB_ICON_BASE_DIR, ('info/info_display.png')) + + self.set_resizable(False) + + self.table = gtk.Table(1,1,False) + self.table.set_row_spacings(0) + self.table.set_col_spacings(0) + + self.image = gtk.Image() + self.image.set_from_file(ICON_PACKAGES_DISPLAY_FILE) + self.image.set_property("xalign",0) + #self.vbox.add(self.image) + + image_info = self.properties.split("*")[0] + info = self.properties.split("*")[1] + + vbox = gtk.VBox(True, spacing=30) + + self.label_short = gtk.Label() + self.label_short.set_line_wrap(False) + self.label_short.set_markup(image_info) + self.label_short.set_property("xalign", 0) + + self.info_label = gtk.Label() + self.info_label.set_line_wrap(True) + self.info_label.set_markup(info) + self.info_label.set_property("yalign", 0.5) + + self.table.attach(self.image, 0,1,0,1, xoptions=gtk.FILL|gtk.EXPAND, yoptions=gtk.FILL,xpadding=5,ypadding=5) + self.table.attach(self.label_short, 0,1,0,1, xoptions=gtk.FILL|gtk.EXPAND, yoptions=gtk.FILL,xpadding=40,ypadding=5) + self.table.attach(self.info_label, 0,1,1,2, xoptions=gtk.FILL|gtk.EXPAND, yoptions=gtk.FILL,xpadding=40,ypadding=10) + + self.vbox.add(self.table) + self.connect('delete-event', lambda w, e: self.destroy() or True) + + def treeViewTooltip( self, widget, e, tooltips, cell, emptyText="" ): + try: + (path,col,x,y) = widget.get_path_at_pos( int(e.x), int(e.y) ) + it = widget.get_model().get_iter(path) + value = widget.get_model().get_value(it,cell) + if value in self.tooltip_items: + tooltips.set_tip(widget, self.tooltip_items[value]) + tooltips.enable() + else: + tooltips.set_tip(widget, emptyText) + except: + tooltips.set_tip(widget, emptyText) + + + def create_package_visual_elements(self): + + import json + + name = self.properties['name'] + binb = self.properties['binb'] + size = self.properties['size'] + recipe = self.properties['recipe'] + file_list = json.loads(self.properties['files_list']) + + files_temp = '' + paths_temp = '' + files_binb = [] + paths_binb = [] + + self.tooltip_items = {} + + self.set_resizable(False) + + #cleaning out the recipe variable + recipe = recipe.split("+")[0] + + vbox = gtk.VBox(True,spacing = 0) + + ###################################### NAME ROW + COL ################################# + + self.label_short = gtk.Label() + self.label_short.set_size_request(300,-1) + self.label_short.set_selectable(True) + self.label_short.set_line_wrap(True) + self.label_short.set_markup("Name: " + name) + self.label_short.set_property("xalign", 0) + + self.vbox.add(self.label_short) + + ###################################### SIZE ROW + COL ###################################### + + self.label_short = gtk.Label() + self.label_short.set_size_request(300,-1) + self.label_short.set_selectable(True) + self.label_short.set_line_wrap(True) + self.label_short.set_markup("Size: " + size) + self.label_short.set_property("xalign", 0) + + self.vbox.add(self.label_short) + + ##################################### RECIPE ROW + COL ######################################### + + self.label_short = gtk.Label() + self.label_short.set_size_request(300,-1) + self.label_short.set_selectable(True) + self.label_short.set_line_wrap(True) + self.label_short.set_markup("Recipe: " + recipe) + self.label_short.set_property("xalign", 0) + + self.vbox.add(self.label_short) + + ##################################### BINB ROW + COL ####################################### + + if binb != '': + self.label_short = gtk.Label() + self.label_short.set_selectable(True) + self.label_short.set_line_wrap(True) + self.label_short.set_markup("Brought in by: ") + self.label_short.set_property("xalign", 0) + + self.label_info = gtk.Label() + self.label_info.set_size_request(300,-1) + self.label_info.set_selectable(True) + self.label_info.set_line_wrap(True) + self.label_info.set_markup(binb) + self.label_info.set_property("xalign", 0) + + self.vbox.add(self.label_short) + self.vbox.add(self.label_info) + + #################################### FILES BROUGHT BY PACKAGES ################################### + + if file_list: + + self.textWindow = gtk.ScrolledWindow() + self.textWindow.set_shadow_type(gtk.SHADOW_IN) + self.textWindow.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) + self.textWindow.set_size_request(100, 170) + + packagefiles_store = gtk.ListStore(str) + + self.packagefiles_tv = gtk.TreeView() + self.packagefiles_tv.set_rules_hint(True) + self.packagefiles_tv.set_headers_visible(True) + self.textWindow.add(self.packagefiles_tv) + + self.cell1 = gtk.CellRendererText() + col1 = gtk.TreeViewColumn('Package files', self.cell1) + col1.set_cell_data_func(self.cell1, self.regex_field) + self.packagefiles_tv.append_column(col1) + + items = file_list.keys() + items.sort() + for item in items: + fullpath = item + while len(item) > 35: + item = item[:len(item)/2] + "" + item[len(item)/2+1:] + if len(item) == 35: + item = item[:len(item)/2] + "..." + item[len(item)/2+3:] + self.tooltip_items[item] = fullpath + + packagefiles_store.append([str(item)]) + + self.packagefiles_tv.set_model(packagefiles_store) + + tips = gtk.Tooltips() + tips.set_tip(self.packagefiles_tv, "") + self.packagefiles_tv.connect("motion-notify-event", self.treeViewTooltip, tips, 0) + self.packagefiles_tv.set_events(gtk.gdk.POINTER_MOTION_MASK) + + self.vbox.add(self.textWindow) + + self.vbox.show_all() + + + def regex_field(self, column, cell, model, iter): + cell.set_property('text', model.get_value(iter, 0)) + return + + + def create_recipe_visual_elements(self): + + summary = self.properties['summary'] + name = self.properties['name'] + version = self.properties['version'] + revision = self.properties['revision'] + binb = self.properties['binb'] + group = self.properties['group'] + license = self.properties['license'] + homepage = self.properties['homepage'] + bugtracker = self.properties['bugtracker'] + description = self.properties['description'] + + self.set_resizable(False) + + #cleaning out the version variable and also the summary + version = version.split(":")[1] + if len(version) > 30: + version = version.split("+")[0] + else: + version = version.split("-")[0] + license = license.replace("&" , "and") + if (homepage == ''): + homepage = 'unknown' + if (bugtracker == ''): + bugtracker = 'unknown' + summary = summary.split("+")[0] + + #calculating the rows needed for the table + binb_items_count = len(binb.split(',')) + binb_items = binb.split(',') + + vbox = gtk.VBox(False,spacing = 0) + + ######################################## SUMMARY LABEL ######################################### + + if summary != '': + self.label_short = gtk.Label() + self.label_short.set_width_chars(37) + self.label_short.set_selectable(True) + self.label_short.set_line_wrap(True) + self.label_short.set_markup("" + summary + "") + self.label_short.set_property("xalign", 0) + + self.vbox.add(self.label_short) + + ########################################## NAME ROW + COL ####################################### + + self.label_short = gtk.Label() + self.label_short.set_selectable(True) + self.label_short.set_line_wrap(True) + self.label_short.set_markup("Name: " + name) + self.label_short.set_property("xalign", 0) + + self.vbox.add(self.label_short) + + ####################################### VERSION ROW + COL #################################### + + self.label_short = gtk.Label() + self.label_short.set_selectable(True) + self.label_short.set_line_wrap(True) + self.label_short.set_markup("Version: " + version) + self.label_short.set_property("xalign", 0) + + self.vbox.add(self.label_short) + + ##################################### REVISION ROW + COL ##################################### + + self.label_short = gtk.Label() + self.label_short.set_line_wrap(True) + self.label_short.set_selectable(True) + self.label_short.set_markup("Revision: " + revision) + self.label_short.set_property("xalign", 0) + + self.vbox.add(self.label_short) + + ################################## GROUP ROW + COL ############################################ + + self.label_short = gtk.Label() + self.label_short.set_selectable(True) + self.label_short.set_line_wrap(True) + self.label_short.set_markup("Group: " + group) + self.label_short.set_property("xalign", 0) + + self.vbox.add(self.label_short) + + ################################# HOMEPAGE ROW + COL ############################################ + + if homepage != 'unknown': + self.label_info = gtk.Label() + self.label_info.set_selectable(True) + self.label_info.set_line_wrap(True) + if len(homepage) > 35: + self.label_info.set_markup("" + homepage[0:35] + "..." + "") + else: + self.label_info.set_markup("" + homepage[0:60] + "") + + self.label_info.set_property("xalign", 0) + + self.label_short = gtk.Label() + self.label_short.set_selectable(True) + self.label_short.set_line_wrap(True) + self.label_short.set_markup("Homepage: ") + self.label_short.set_property("xalign", 0) + + self.vbox.add(self.label_short) + self.vbox.add(self.label_info) + + ################################# BUGTRACKER ROW + COL ########################################### + + if bugtracker != 'unknown': + self.label_info = gtk.Label() + self.label_info.set_selectable(True) + self.label_info.set_line_wrap(True) + if len(bugtracker) > 35: + self.label_info.set_markup("" + bugtracker[0:35] + "..." + "") + else: + self.label_info.set_markup("" + bugtracker[0:60] + "") + self.label_info.set_property("xalign", 0) + + self.label_short = gtk.Label() + self.label_short.set_selectable(True) + self.label_short.set_line_wrap(True) + self.label_short.set_markup("Bugtracker: ") + self.label_short.set_property("xalign", 0) + + self.vbox.add(self.label_short) + self.vbox.add(self.label_info) + + ################################# LICENSE ROW + COL ############################################ + + self.label_info = gtk.Label() + self.label_info.set_selectable(True) + self.label_info.set_line_wrap(True) + self.label_info.set_markup(license) + self.label_info.set_property("xalign", 0) + + self.label_short = gtk.Label() + self.label_short.set_selectable(True) + self.label_short.set_line_wrap(True) + self.label_short.set_markup("License: ") + self.label_short.set_property("xalign", 0) + + self.vbox.add(self.label_short) + self.vbox.add(self.label_info) + + ################################### BINB ROW+COL ############################################# + + if binb != '': + self.label_short = gtk.Label() + self.label_short.set_selectable(True) + self.label_short.set_line_wrap(True) + self.label_short.set_markup("Brought in by: ") + self.label_short.set_property("xalign", 0) + self.vbox.add(self.label_short) + self.label_info = gtk.Label() + self.label_info.set_selectable(True) + self.label_info.set_width_chars(36) + if len(binb) > 200: + scrolled_window = gtk.ScrolledWindow() + scrolled_window.set_policy(gtk.POLICY_NEVER,gtk.POLICY_ALWAYS) + scrolled_window.set_size_request(100,100) + self.label_info.set_markup(binb) + self.label_info.set_padding(6,6) + self.label_info.set_alignment(0,0) + self.label_info.set_line_wrap(True) + scrolled_window.add_with_viewport(self.label_info) + self.vbox.add(scrolled_window) + else: + self.label_info.set_markup(binb) + self.label_info.set_property("xalign", 0) + self.label_info.set_line_wrap(True) + self.vbox.add(self.label_info) + + ################################ DESCRIPTION TAG ROW ################################################# + + self.label_short = gtk.Label() + self.label_short.set_line_wrap(True) + self.label_short.set_markup("Description ") + self.label_short.set_property("xalign", 0) + self.vbox.add(self.label_short) + + ################################ DESCRIPTION INFORMATION ROW ########################################## + + hbox = gtk.HBox(True,spacing = 0) + + self.label_short = gtk.Label() + self.label_short.set_selectable(True) + self.label_short.set_width_chars(36) + if len(description) > 200: + scrolled_window = gtk.ScrolledWindow() + scrolled_window.set_policy(gtk.POLICY_NEVER,gtk.POLICY_ALWAYS) + scrolled_window.set_size_request(100,100) + self.label_short.set_markup(description) + self.label_short.set_padding(6,6) + self.label_short.set_alignment(0,0) + self.label_short.set_line_wrap(True) + scrolled_window.add_with_viewport(self.label_short) + self.vbox.add(scrolled_window) + else: + self.label_short.set_markup(description) + self.label_short.set_property("xalign", 0) + self.label_short.set_line_wrap(True) + self.vbox.add(self.label_short) + + self.vbox.show_all() diff --git a/bitbake/lib/bb/ui/crumbs/hig/proxydetailsdialog.py b/bitbake/lib/bb/ui/crumbs/hig/proxydetailsdialog.py new file mode 100644 index 0000000000..69e7dffb6d --- /dev/null +++ b/bitbake/lib/bb/ui/crumbs/hig/proxydetailsdialog.py @@ -0,0 +1,90 @@ +# +# BitBake Graphical GTK User Interface +# +# Copyright (C) 2011-2012 Intel Corporation +# +# Authored by Joshua Lock +# Authored by Dongxiao Xu +# Authored by Shane Wang +# +# 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 +from bb.ui.crumbs.hig.crumbsdialog import CrumbsDialog + +""" +The following are convenience classes for implementing GNOME HIG compliant +BitBake GUI's +In summary: spacing = 12px, border-width = 6px +""" + +class ProxyDetailsDialog (CrumbsDialog): + + def __init__(self, title, user, passwd, parent, flags, buttons=None): + super(ProxyDetailsDialog, self).__init__(title, parent, flags, buttons) + self.connect("response", self.response_cb) + + self.auth = not (user == None or passwd == None or user == "") + self.user = user or "" + self.passwd = passwd or "" + + # create visual elements on the dialog + self.create_visual_elements() + + def create_visual_elements(self): + self.auth_checkbox = gtk.CheckButton("Use authentication") + self.auth_checkbox.set_tooltip_text("Check this box to set the username and the password") + self.auth_checkbox.set_active(self.auth) + self.auth_checkbox.connect("toggled", self.auth_checkbox_toggled_cb) + self.vbox.pack_start(self.auth_checkbox, expand=False, fill=False) + + hbox = gtk.HBox(False, 6) + self.user_label = gtk.Label("Username:") + self.user_text = gtk.Entry() + self.user_text.set_text(self.user) + hbox.pack_start(self.user_label, expand=False, fill=False) + hbox.pack_end(self.user_text, expand=False, fill=False) + self.vbox.pack_start(hbox, expand=False, fill=False) + + hbox = gtk.HBox(False, 6) + self.passwd_label = gtk.Label("Password:") + self.passwd_text = gtk.Entry() + self.passwd_text.set_text(self.passwd) + hbox.pack_start(self.passwd_label, expand=False, fill=False) + hbox.pack_end(self.passwd_text, expand=False, fill=False) + self.vbox.pack_start(hbox, expand=False, fill=False) + + self.refresh_auth_components() + self.show_all() + + def refresh_auth_components(self): + self.user_label.set_sensitive(self.auth) + self.user_text.set_editable(self.auth) + self.user_text.set_sensitive(self.auth) + self.passwd_label.set_sensitive(self.auth) + self.passwd_text.set_editable(self.auth) + self.passwd_text.set_sensitive(self.auth) + + def auth_checkbox_toggled_cb(self, button): + self.auth = self.auth_checkbox.get_active() + self.refresh_auth_components() + + def response_cb(self, dialog, response_id): + if response_id == gtk.RESPONSE_OK: + if self.auth: + self.user = self.user_text.get_text() + self.passwd = self.passwd_text.get_text() + else: + self.user = None + self.passwd = None diff --git a/bitbake/lib/bb/ui/crumbs/hig/retrieveimagedialog.py b/bitbake/lib/bb/ui/crumbs/hig/retrieveimagedialog.py new file mode 100644 index 0000000000..9017139850 --- /dev/null +++ b/bitbake/lib/bb/ui/crumbs/hig/retrieveimagedialog.py @@ -0,0 +1,51 @@ +# +# BitBake Graphical GTK User Interface +# +# Copyright (C) 2013 Intel Corporation +# +# Authored by Cristiana Voicu +# +# 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 + +class RetrieveImageDialog (gtk.FileChooserDialog): + """ + This class is used to create a dialog that permits to retrieve + a custom image saved previously from Hob. + """ + def __init__(self, directory,title, parent, flags, buttons=None): + super(RetrieveImageDialog, self).__init__(title, None, gtk.FILE_CHOOSER_ACTION_OPEN, + (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,gtk.STOCK_OPEN, gtk.RESPONSE_OK)) + self.directory = directory + + # create visual elements on the dialog + self.create_visual_elements() + + def create_visual_elements(self): + self.set_show_hidden(True) + self.set_default_response(gtk.RESPONSE_OK) + self.set_current_folder(self.directory) + + vbox = self.get_children()[0].get_children()[0].get_children()[0] + for child in vbox.get_children()[0].get_children()[0].get_children()[0].get_children(): + vbox.get_children()[0].get_children()[0].get_children()[0].remove(child) + + label1 = gtk.Label() + label1.set_text("File system" + self.directory) + label1.show() + vbox.get_children()[0].get_children()[0].get_children()[0].pack_start(label1, expand=False, fill=False, padding=0) + vbox.get_children()[0].get_children()[1].get_children()[0].hide() + + self.get_children()[0].get_children()[1].get_children()[0].set_label("Select") diff --git a/bitbake/lib/bb/ui/crumbs/hig/saveimagedialog.py b/bitbake/lib/bb/ui/crumbs/hig/saveimagedialog.py new file mode 100644 index 0000000000..4195f70e1e --- /dev/null +++ b/bitbake/lib/bb/ui/crumbs/hig/saveimagedialog.py @@ -0,0 +1,159 @@ +# +# BitBake Graphical GTK User Interface +# +# Copyright (C) 2013 Intel Corporation +# +# Authored by Cristiana Voicu +# +# 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 glib +from bb.ui.crumbs.hig.crumbsdialog import CrumbsDialog +from bb.ui.crumbs.hig.crumbsmessagedialog import CrumbsMessageDialog +from bb.ui.crumbs.hobwidget import HobButton + +class SaveImageDialog (CrumbsDialog): + """ + This class is used to create a dialog that permits to save + a custom image in a predefined directory. + """ + def __init__(self, directory, name, description, title, parent, flags, buttons=None): + super(SaveImageDialog, self).__init__(title, parent, flags, buttons) + self.directory = directory + self.builder = parent + self.name_field = name + self.description_field = description + + # create visual elements on the dialog + self.create_visual_elements() + + def create_visual_elements(self): + self.set_default_response(gtk.RESPONSE_OK) + self.vbox.set_border_width(6) + + sub_vbox = gtk.VBox(False, 12) + self.vbox.pack_start(sub_vbox, expand=False, fill=False) + label = gtk.Label() + label.set_alignment(0, 0) + label.set_markup("Name") + sub_label = gtk.Label() + sub_label.set_alignment(0, 0) + content = "Image recipe names should be all lowercase and include only alphanumeric\n" + content += "characters. The only special character you can use is the ASCII hyphen (-)." + sub_label.set_markup(content) + self.name_entry = gtk.Entry() + self.name_entry.set_text(self.name_field) + self.name_entry.set_size_request(350,30) + self.name_entry.connect("changed", self.name_entry_changed) + sub_vbox.pack_start(label, expand=False, fill=False) + sub_vbox.pack_start(sub_label, expand=False, fill=False) + sub_vbox.pack_start(self.name_entry, expand=False, fill=False) + + sub_vbox = gtk.VBox(False, 12) + self.vbox.pack_start(sub_vbox, expand=False, fill=False) + label = gtk.Label() + label.set_alignment(0, 0) + label.set_markup("Description (optional)") + sub_label = gtk.Label() + sub_label.set_alignment(0, 0) + sub_label.set_markup("The description should be less than 150 characters long.") + self.description_entry = gtk.TextView() + description_buffer = self.description_entry.get_buffer() + description_buffer.set_text(self.description_field) + description_buffer.connect("insert-text", self.limit_description_length) + self.description_entry.set_wrap_mode(gtk.WRAP_WORD) + self.description_entry.set_size_request(350,50) + sub_vbox.pack_start(label, expand=False, fill=False) + sub_vbox.pack_start(sub_label, expand=False, fill=False) + sub_vbox.pack_start(self.description_entry, expand=False, fill=False) + + sub_vbox = gtk.VBox(False, 12) + self.vbox.pack_start(sub_vbox, expand=False, fill=False) + label = gtk.Label() + label.set_alignment(0, 0) + label.set_markup("Your image recipe will be saved to:") + sub_label = gtk.Label() + sub_label.set_alignment(0, 0) + sub_label.set_markup(self.directory) + sub_vbox.pack_start(label, expand=False, fill=False) + sub_vbox.pack_start(sub_label, expand=False, fill=False) + + table = gtk.Table(1, 4, True) + + cancel_button = gtk.Button() + cancel_button.set_label("Cancel") + cancel_button.connect("clicked", self.cancel_button_cb) + cancel_button.set_size_request(110, 30) + + self.save_button = gtk.Button() + self.save_button.set_label("Save") + self.save_button.connect("clicked", self.save_button_cb) + self.save_button.set_size_request(110, 30) + if self.name_entry.get_text() == '': + self.save_button.set_sensitive(False) + + table.attach(cancel_button, 2, 3, 0, 1) + table.attach(self.save_button, 3, 4, 0, 1) + self.vbox.pack_end(table, expand=False, fill=False) + + self.show_all() + + def limit_description_length(self, textbuffer, iter, text, length): + buffer_bounds = textbuffer.get_bounds() + entire_text = textbuffer.get_text(*buffer_bounds) + entire_text += text + if len(entire_text)>150 or text=="\n": + textbuffer.emit_stop_by_name("insert-text") + + def name_entry_changed(self, entry): + text = entry.get_text() + if text == '': + self.save_button.set_sensitive(False) + else: + self.save_button.set_sensitive(True) + + def cancel_button_cb(self, button): + self.destroy() + + def save_button_cb(self, button): + text = self.name_entry.get_text() + new_text = text.replace("-","") + description_buffer = self.description_entry.get_buffer() + description = description_buffer.get_text(description_buffer.get_start_iter(),description_buffer.get_end_iter()) + if new_text.islower() and new_text.isalnum(): + self.builder.image_details_page.image_saved = True + self.builder.customized = False + self.builder.generate_new_image(self.directory+text, description) + self.builder.recipe_model.set_in_list(text, description) + self.builder.recipe_model.set_selected_image(text) + self.builder.image_details_page.show_page(self.builder.IMAGE_GENERATED) + self.builder.image_details_page.name_field_template = text + self.builder.image_details_page.description_field_template = description + self.destroy() + else: + self.show_invalid_input_error_dialog() + + def show_invalid_input_error_dialog(self): + lbl = "Invalid characters in image recipe name" + msg = "Image recipe names should be all lowercase and\n" + msg += "include only alphanumeric characters. The only\n" + msg += "special character you can use is the ASCII hyphen (-)." + dialog = CrumbsMessageDialog(self, lbl, gtk.MESSAGE_ERROR, msg) + button = dialog.add_button("Close", gtk.RESPONSE_OK) + HobButton.style_button(button) + + res = dialog.run() + self.name_entry.grab_focus() + dialog.destroy() diff --git a/bitbake/lib/bb/ui/crumbs/hig/settingsuihelper.py b/bitbake/lib/bb/ui/crumbs/hig/settingsuihelper.py new file mode 100644 index 0000000000..e0285c93ce --- /dev/null +++ b/bitbake/lib/bb/ui/crumbs/hig/settingsuihelper.py @@ -0,0 +1,122 @@ +# +# BitBake Graphical GTK User Interface +# +# Copyright (C) 2011-2012 Intel Corporation +# +# Authored by Joshua Lock +# Authored by Dongxiao Xu +# Authored by Shane Wang +# +# 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 os +from bb.ui.crumbs.hobwidget import HobInfoButton, HobButton, HobAltButton + +""" +The following are convenience classes for implementing GNOME HIG compliant +BitBake GUI's +In summary: spacing = 12px, border-width = 6px +""" + +class SettingsUIHelper(): + + def gen_label_widget(self, content): + label = gtk.Label() + label.set_alignment(0, 0) + label.set_markup(content) + label.show() + return label + + def gen_label_info_widget(self, content, tooltip): + table = gtk.Table(1, 10, False) + label = self.gen_label_widget(content) + info = HobInfoButton(tooltip, self) + table.attach(label, 0, 1, 0, 1, xoptions=gtk.FILL) + table.attach(info, 1, 2, 0, 1, xoptions=gtk.FILL, xpadding=10) + return table + + def gen_spinner_widget(self, content, lower, upper, tooltip=""): + hbox = gtk.HBox(False, 12) + adjust = gtk.Adjustment(value=content, lower=lower, upper=upper, step_incr=1) + spinner = gtk.SpinButton(adjustment=adjust, climb_rate=1, digits=0) + + spinner.set_value(content) + hbox.pack_start(spinner, expand=False, fill=False) + + info = HobInfoButton(tooltip, self) + hbox.pack_start(info, expand=False, fill=False) + + hbox.show_all() + return hbox, spinner + + def gen_combo_widget(self, curr_item, all_item, tooltip=""): + hbox = gtk.HBox(False, 12) + combo = gtk.combo_box_new_text() + hbox.pack_start(combo, expand=False, fill=False) + + index = 0 + for item in all_item or []: + combo.append_text(item) + if item == curr_item: + combo.set_active(index) + index += 1 + + info = HobInfoButton(tooltip, self) + hbox.pack_start(info, expand=False, fill=False) + + hbox.show_all() + return hbox, combo + + def entry_widget_select_path_cb(self, action, parent, entry): + dialog = gtk.FileChooserDialog("", parent, + gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER) + text = entry.get_text() + dialog.set_current_folder(text if len(text) > 0 else os.getcwd()) + button = dialog.add_button("Cancel", gtk.RESPONSE_NO) + HobAltButton.style_button(button) + button = dialog.add_button("Open", gtk.RESPONSE_YES) + HobButton.style_button(button) + response = dialog.run() + if response == gtk.RESPONSE_YES: + path = dialog.get_filename() + entry.set_text(path) + + dialog.destroy() + + def gen_entry_widget(self, content, parent, tooltip="", need_button=True): + hbox = gtk.HBox(False, 12) + entry = gtk.Entry() + entry.set_text(content) + entry.set_size_request(350,30) + + if need_button: + table = gtk.Table(1, 10, False) + hbox.pack_start(table, expand=True, fill=True) + table.attach(entry, 0, 9, 0, 1, xoptions=gtk.SHRINK) + image = gtk.Image() + image.set_from_stock(gtk.STOCK_OPEN,gtk.ICON_SIZE_BUTTON) + open_button = gtk.Button() + open_button.set_image(image) + open_button.connect("clicked", self.entry_widget_select_path_cb, parent, entry) + table.attach(open_button, 9, 10, 0, 1, xoptions=gtk.SHRINK) + else: + hbox.pack_start(entry, expand=True, fill=True) + + if tooltip != "": + info = HobInfoButton(tooltip, self) + hbox.pack_start(info, expand=False, fill=False) + + hbox.show_all() + return hbox, entry diff --git a/bitbake/lib/bb/ui/crumbs/hig/simplesettingsdialog.py b/bitbake/lib/bb/ui/crumbs/hig/simplesettingsdialog.py new file mode 100644 index 0000000000..ab5b614c8d --- /dev/null +++ b/bitbake/lib/bb/ui/crumbs/hig/simplesettingsdialog.py @@ -0,0 +1,894 @@ +# +# BitBake Graphical GTK User Interface +# +# Copyright (C) 2011-2012 Intel Corporation +# +# Authored by Joshua Lock +# Authored by Dongxiao Xu +# Authored by Shane Wang +# +# 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 hashlib +from bb.ui.crumbs.hobwidget import hic, HobInfoButton, HobButton, HobAltButton +from bb.ui.crumbs.progressbar import HobProgressBar +from bb.ui.crumbs.hig.settingsuihelper import SettingsUIHelper +from bb.ui.crumbs.hig.crumbsdialog import CrumbsDialog +from bb.ui.crumbs.hig.crumbsmessagedialog import CrumbsMessageDialog +from bb.ui.crumbs.hig.proxydetailsdialog import ProxyDetailsDialog + +""" +The following are convenience classes for implementing GNOME HIG compliant +BitBake GUI's +In summary: spacing = 12px, border-width = 6px +""" + +class SimpleSettingsDialog (CrumbsDialog, SettingsUIHelper): + + (BUILD_ENV_PAGE_ID, + SHARED_STATE_PAGE_ID, + PROXIES_PAGE_ID, + OTHERS_PAGE_ID) = range(4) + + (TEST_NETWORK_NONE, + TEST_NETWORK_INITIAL, + TEST_NETWORK_RUNNING, + TEST_NETWORK_PASSED, + TEST_NETWORK_FAILED, + TEST_NETWORK_CANCELED) = range(6) + + TARGETS = [ + ("MY_TREE_MODEL_ROW", gtk.TARGET_SAME_WIDGET, 0), + ("text/plain", 0, 1), + ("TEXT", 0, 2), + ("STRING", 0, 3), + ] + + def __init__(self, title, configuration, all_image_types, + all_package_formats, all_distros, all_sdk_machines, + max_threads, parent, flags, handler, buttons=None): + super(SimpleSettingsDialog, self).__init__(title, parent, flags, buttons) + + # class members from other objects + # bitbake settings from Builder.Configuration + self.configuration = configuration + self.image_types = all_image_types + self.all_package_formats = all_package_formats + self.all_distros = all_distros + self.all_sdk_machines = all_sdk_machines + self.max_threads = max_threads + + # class members for internal use + self.dldir_text = None + self.sstatedir_text = None + self.sstatemirrors_list = [] + self.sstatemirrors_changed = 0 + self.bb_spinner = None + self.pmake_spinner = None + self.rootfs_size_spinner = None + self.extra_size_spinner = None + self.gplv3_checkbox = None + self.toolchain_checkbox = None + self.setting_store = None + self.image_types_checkbuttons = {} + + self.md5 = self.config_md5() + self.proxy_md5 = self.config_proxy_md5() + self.settings_changed = False + self.proxy_settings_changed = False + self.handler = handler + self.proxy_test_ran = False + self.selected_mirror_row = 0 + self.new_mirror = False + + # create visual elements on the dialog + self.create_visual_elements() + self.connect("response", self.response_cb) + + def _get_sorted_value(self, var): + return " ".join(sorted(str(var).split())) + "\n" + + def config_proxy_md5(self): + data = ("ENABLE_PROXY: " + self._get_sorted_value(self.configuration.enable_proxy)) + if self.configuration.enable_proxy: + for protocol in self.configuration.proxies.keys(): + data += (protocol + ": " + self._get_sorted_value(self.configuration.combine_proxy(protocol))) + return hashlib.md5(data).hexdigest() + + def config_md5(self): + data = "" + for key in self.configuration.extra_setting.keys(): + data += (key + ": " + self._get_sorted_value(self.configuration.extra_setting[key])) + return hashlib.md5(data).hexdigest() + + def gen_proxy_entry_widget(self, protocol, parent, need_button=True, line=0): + label = gtk.Label(protocol.upper() + " proxy") + self.proxy_table.attach(label, 0, 1, line, line+1, xpadding=24) + + proxy_entry = gtk.Entry() + proxy_entry.set_size_request(300, -1) + self.proxy_table.attach(proxy_entry, 1, 2, line, line+1, ypadding=4) + + self.proxy_table.attach(gtk.Label(":"), 2, 3, line, line+1, xpadding=12, ypadding=4) + + port_entry = gtk.Entry() + port_entry.set_size_request(60, -1) + self.proxy_table.attach(port_entry, 3, 4, line, line+1, ypadding=4) + + details_button = HobAltButton("Details") + details_button.connect("clicked", self.details_cb, parent, protocol) + self.proxy_table.attach(details_button, 4, 5, line, line+1, xpadding=4, yoptions=gtk.EXPAND) + + return proxy_entry, port_entry, details_button + + def refresh_proxy_components(self): + self.same_checkbox.set_sensitive(self.configuration.enable_proxy) + + self.http_proxy.set_text(self.configuration.combine_host_only("http")) + self.http_proxy.set_editable(self.configuration.enable_proxy) + self.http_proxy.set_sensitive(self.configuration.enable_proxy) + self.http_proxy_port.set_text(self.configuration.combine_port_only("http")) + self.http_proxy_port.set_editable(self.configuration.enable_proxy) + self.http_proxy_port.set_sensitive(self.configuration.enable_proxy) + self.http_proxy_details.set_sensitive(self.configuration.enable_proxy) + + self.https_proxy.set_text(self.configuration.combine_host_only("https")) + self.https_proxy.set_editable(self.configuration.enable_proxy and (not self.configuration.same_proxy)) + self.https_proxy.set_sensitive(self.configuration.enable_proxy and (not self.configuration.same_proxy)) + self.https_proxy_port.set_text(self.configuration.combine_port_only("https")) + self.https_proxy_port.set_editable(self.configuration.enable_proxy and (not self.configuration.same_proxy)) + self.https_proxy_port.set_sensitive(self.configuration.enable_proxy and (not self.configuration.same_proxy)) + self.https_proxy_details.set_sensitive(self.configuration.enable_proxy and (not self.configuration.same_proxy)) + + self.ftp_proxy.set_text(self.configuration.combine_host_only("ftp")) + self.ftp_proxy.set_editable(self.configuration.enable_proxy and (not self.configuration.same_proxy)) + self.ftp_proxy.set_sensitive(self.configuration.enable_proxy and (not self.configuration.same_proxy)) + self.ftp_proxy_port.set_text(self.configuration.combine_port_only("ftp")) + self.ftp_proxy_port.set_editable(self.configuration.enable_proxy and (not self.configuration.same_proxy)) + self.ftp_proxy_port.set_sensitive(self.configuration.enable_proxy and (not self.configuration.same_proxy)) + self.ftp_proxy_details.set_sensitive(self.configuration.enable_proxy and (not self.configuration.same_proxy)) + + self.socks_proxy.set_text(self.configuration.combine_host_only("socks")) + self.socks_proxy.set_editable(self.configuration.enable_proxy and (not self.configuration.same_proxy)) + self.socks_proxy.set_sensitive(self.configuration.enable_proxy and (not self.configuration.same_proxy)) + self.socks_proxy_port.set_text(self.configuration.combine_port_only("socks")) + self.socks_proxy_port.set_editable(self.configuration.enable_proxy and (not self.configuration.same_proxy)) + self.socks_proxy_port.set_sensitive(self.configuration.enable_proxy and (not self.configuration.same_proxy)) + self.socks_proxy_details.set_sensitive(self.configuration.enable_proxy and (not self.configuration.same_proxy)) + + self.cvs_proxy.set_text(self.configuration.combine_host_only("cvs")) + self.cvs_proxy.set_editable(self.configuration.enable_proxy and (not self.configuration.same_proxy)) + self.cvs_proxy.set_sensitive(self.configuration.enable_proxy and (not self.configuration.same_proxy)) + self.cvs_proxy_port.set_text(self.configuration.combine_port_only("cvs")) + self.cvs_proxy_port.set_editable(self.configuration.enable_proxy and (not self.configuration.same_proxy)) + self.cvs_proxy_port.set_sensitive(self.configuration.enable_proxy and (not self.configuration.same_proxy)) + self.cvs_proxy_details.set_sensitive(self.configuration.enable_proxy and (not self.configuration.same_proxy)) + + if self.configuration.same_proxy: + if self.http_proxy.get_text(): + [w.set_text(self.http_proxy.get_text()) for w in self.same_proxy_addresses] + if self.http_proxy_port.get_text(): + [w.set_text(self.http_proxy_port.get_text()) for w in self.same_proxy_ports] + + def proxy_checkbox_toggled_cb(self, button): + self.configuration.enable_proxy = self.proxy_checkbox.get_active() + if not self.configuration.enable_proxy: + self.configuration.same_proxy = False + self.same_checkbox.set_active(self.configuration.same_proxy) + self.save_proxy_data() + self.refresh_proxy_components() + + def same_checkbox_toggled_cb(self, button): + self.configuration.same_proxy = self.same_checkbox.get_active() + self.save_proxy_data() + self.refresh_proxy_components() + + def save_proxy_data(self): + self.configuration.split_proxy("http", self.http_proxy.get_text() + ":" + self.http_proxy_port.get_text()) + if self.configuration.same_proxy: + self.configuration.split_proxy("https", self.http_proxy.get_text() + ":" + self.http_proxy_port.get_text()) + self.configuration.split_proxy("ftp", self.http_proxy.get_text() + ":" + self.http_proxy_port.get_text()) + self.configuration.split_proxy("socks", self.http_proxy.get_text() + ":" + self.http_proxy_port.get_text()) + self.configuration.split_proxy("cvs", self.http_proxy.get_text() + ":" + self.http_proxy_port.get_text()) + else: + self.configuration.split_proxy("https", self.https_proxy.get_text() + ":" + self.https_proxy_port.get_text()) + self.configuration.split_proxy("ftp", self.ftp_proxy.get_text() + ":" + self.ftp_proxy_port.get_text()) + self.configuration.split_proxy("socks", self.socks_proxy.get_text() + ":" + self.socks_proxy_port.get_text()) + self.configuration.split_proxy("cvs", self.cvs_proxy.get_text() + ":" + self.cvs_proxy_port.get_text()) + + def response_cb(self, dialog, response_id): + if response_id == gtk.RESPONSE_YES: + if self.proxy_checkbox.get_active(): + # Check that all proxy entries have a corresponding port + for proxy, port in zip(self.all_proxy_addresses, self.all_proxy_ports): + if proxy.get_text() and not port.get_text(): + lbl = "Enter all port numbers" + msg = "Proxy servers require a port number. Please make sure you have entered a port number for each proxy server." + dialog = CrumbsMessageDialog(self, lbl, gtk.MESSAGE_WARNING, msg) + button = dialog.add_button("Close", gtk.RESPONSE_OK) + HobButton.style_button(button) + response = dialog.run() + dialog.destroy() + self.emit_stop_by_name("response") + return + + self.configuration.dldir = self.dldir_text.get_text() + self.configuration.sstatedir = self.sstatedir_text.get_text() + self.configuration.sstatemirror = "" + for mirror in self.sstatemirrors_list: + if mirror[1] != "" and mirror[2].startswith("file://"): + if mirror[1].endswith("\\1"): + smirror = mirror[2] + " " + mirror[1] + " \\n " + else: + smirror = mirror[2] + " " + mirror[1] + "\\1 \\n " + self.configuration.sstatemirror += smirror + self.configuration.bbthread = self.bb_spinner.get_value_as_int() + self.configuration.pmake = self.pmake_spinner.get_value_as_int() + self.save_proxy_data() + self.configuration.extra_setting = {} + it = self.setting_store.get_iter_first() + while it: + key = self.setting_store.get_value(it, 0) + value = self.setting_store.get_value(it, 1) + self.configuration.extra_setting[key] = value + it = self.setting_store.iter_next(it) + + md5 = self.config_md5() + self.settings_changed = (self.md5 != md5) + self.proxy_settings_changed = (self.proxy_md5 != self.config_proxy_md5()) + + def create_build_environment_page(self): + advanced_vbox = gtk.VBox(False, 6) + advanced_vbox.set_border_width(6) + + advanced_vbox.pack_start(self.gen_label_widget('Parallel threads'), expand=False, fill=False) + sub_vbox = gtk.VBox(False, 6) + advanced_vbox.pack_start(sub_vbox, expand=False, fill=False) + label = self.gen_label_widget("BitBake parallel threads") + tooltip = "Sets the number of threads that BitBake tasks can simultaneously run. See the Poky reference manual for information" + bbthread_widget, self.bb_spinner = self.gen_spinner_widget(self.configuration.bbthread, 1, self.max_threads,"BitBake prallalel threads" + "*" + tooltip) + sub_vbox.pack_start(label, expand=False, fill=False) + sub_vbox.pack_start(bbthread_widget, expand=False, fill=False) + + sub_vbox = gtk.VBox(False, 6) + advanced_vbox.pack_start(sub_vbox, expand=False, fill=False) + label = self.gen_label_widget("Make parallel threads") + tooltip = "Sets the maximum number of threads the host can use during the build. See the Poky reference manual for information" + pmake_widget, self.pmake_spinner = self.gen_spinner_widget(self.configuration.pmake, 1, self.max_threads,"Make parallel threads" + "*" + tooltip) + sub_vbox.pack_start(label, expand=False, fill=False) + sub_vbox.pack_start(pmake_widget, expand=False, fill=False) + + advanced_vbox.pack_start(self.gen_label_widget('Downloaded source code'), expand=False, fill=False) + sub_vbox = gtk.VBox(False, 6) + advanced_vbox.pack_start(sub_vbox, expand=False, fill=False) + label = self.gen_label_widget("Downloads directory") + tooltip = "Select a folder that caches the upstream project source code" + dldir_widget, self.dldir_text = self.gen_entry_widget(self.configuration.dldir, self,"Downloaded source code" + "*" + tooltip) + sub_vbox.pack_start(label, expand=False, fill=False) + sub_vbox.pack_start(dldir_widget, expand=False, fill=False) + + return advanced_vbox + + def create_shared_state_page(self): + advanced_vbox = gtk.VBox(False) + advanced_vbox.set_border_width(12) + + sub_vbox = gtk.VBox(False) + advanced_vbox.pack_start(sub_vbox, expand=False, fill=False, padding=24) + content = "Shared state directory" + tooltip = "Select a folder that caches your prebuilt results" + label = self.gen_label_info_widget(content,"Shared state directory" + "*" + tooltip) + sstatedir_widget, self.sstatedir_text = self.gen_entry_widget(self.configuration.sstatedir, self) + sub_vbox.pack_start(label, expand=False, fill=False) + sub_vbox.pack_start(sstatedir_widget, expand=False, fill=False, padding=6) + + content = "Shared state mirrors" + tooltip = "URLs pointing to pre-built mirrors that will speed your build. " + tooltip += "Select the \'Standard\' configuration if the structure of your " + tooltip += "mirror replicates the structure of your local shared state directory. " + tooltip += "For more information on shared state mirrors, check the Yocto Project Reference Manual." + table = self.gen_label_info_widget(content,"Shared state mirrors" + "*" + tooltip) + advanced_vbox.pack_start(table, expand=False, fill=False, padding=6) + + sub_vbox = gtk.VBox(False) + advanced_vbox.pack_start(sub_vbox, gtk.TRUE, gtk.TRUE, 0) + + if self.sstatemirrors_changed == 0: + self.sstatemirrors_changed = 1 + sstatemirrors = self.configuration.sstatemirror + if sstatemirrors == "": + sm_list = ["Standard", "", "file://(.*)"] + self.sstatemirrors_list.append(sm_list) + else: + sstatemirrors = [x for x in sstatemirrors.split('\\n')] + for sstatemirror in sstatemirrors: + sstatemirror_fields = [x for x in sstatemirror.split(' ') if x.strip()] + if len(sstatemirror_fields) == 2: + if sstatemirror_fields[0] == "file://(.*)" or sstatemirror_fields[0] == "file://.*": + sm_list = ["Standard", sstatemirror_fields[1], sstatemirror_fields[0]] + else: + sm_list = ["Custom", sstatemirror_fields[1], sstatemirror_fields[0]] + self.sstatemirrors_list.append(sm_list) + + sstatemirrors_widget, sstatemirrors_store = self.gen_shared_sstate_widget(self.sstatemirrors_list, self) + sub_vbox.pack_start(sstatemirrors_widget, expand=True, fill=True) + + table = gtk.Table(1, 10, False) + table.set_col_spacings(6) + add_mirror_button = HobAltButton("Add mirror") + add_mirror_button.connect("clicked", self.add_mirror) + add_mirror_button.set_size_request(120,30) + table.attach(add_mirror_button, 1, 2, 0, 1, xoptions=gtk.SHRINK) + + self.delete_button = HobAltButton("Delete mirror") + self.delete_button.connect("clicked", self.delete_cb) + self.delete_button.set_size_request(120, 30) + table.attach(self.delete_button, 3, 4, 0, 1, xoptions=gtk.SHRINK) + + advanced_vbox.pack_start(table, expand=False, fill=False, padding=6) + + return advanced_vbox + + def gen_shared_sstate_widget(self, sstatemirrors_list, window): + hbox = gtk.HBox(False) + + sstatemirrors_store = gtk.ListStore(str, str, str) + for sstatemirror in sstatemirrors_list: + sstatemirrors_store.append(sstatemirror) + + self.sstatemirrors_tv = gtk.TreeView() + self.sstatemirrors_tv.set_rules_hint(True) + self.sstatemirrors_tv.set_headers_visible(True) + tree_selection = self.sstatemirrors_tv.get_selection() + tree_selection.set_mode(gtk.SELECTION_SINGLE) + + # Allow enable drag and drop of rows including row move + self.sstatemirrors_tv.enable_model_drag_source( gtk.gdk.BUTTON1_MASK, + self.TARGETS, + gtk.gdk.ACTION_DEFAULT| + gtk.gdk.ACTION_MOVE) + self.sstatemirrors_tv.enable_model_drag_dest(self.TARGETS, + gtk.gdk.ACTION_DEFAULT) + self.sstatemirrors_tv.connect("drag_data_get", self.drag_data_get_cb) + self.sstatemirrors_tv.connect("drag_data_received", self.drag_data_received_cb) + + + self.scroll = gtk.ScrolledWindow() + self.scroll.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC) + self.scroll.set_shadow_type(gtk.SHADOW_IN) + self.scroll.connect('size-allocate', self.scroll_changed) + self.scroll.add(self.sstatemirrors_tv) + + #list store for cell renderer + m = gtk.ListStore(gobject.TYPE_STRING) + m.append(["Standard"]) + m.append(["Custom"]) + + cell0 = gtk.CellRendererCombo() + cell0.set_property("model",m) + cell0.set_property("text-column", 0) + cell0.set_property("editable", True) + cell0.set_property("has-entry", False) + col0 = gtk.TreeViewColumn("Configuration") + col0.pack_start(cell0, False) + col0.add_attribute(cell0, "text", 0) + col0.set_cell_data_func(cell0, self.configuration_field) + self.sstatemirrors_tv.append_column(col0) + + cell0.connect("edited", self.combo_changed, sstatemirrors_store) + + self.cell1 = gtk.CellRendererText() + self.cell1.set_padding(5,2) + col1 = gtk.TreeViewColumn('Regex', self.cell1) + col1.set_cell_data_func(self.cell1, self.regex_field) + self.sstatemirrors_tv.append_column(col1) + + self.cell1.connect("edited", self.regex_changed, sstatemirrors_store) + + cell2 = gtk.CellRendererText() + cell2.set_padding(5,2) + cell2.set_property("editable", True) + col2 = gtk.TreeViewColumn('URL', cell2) + col2.set_cell_data_func(cell2, self.url_field) + self.sstatemirrors_tv.append_column(col2) + + cell2.connect("edited", self.url_changed, sstatemirrors_store) + + self.sstatemirrors_tv.set_model(sstatemirrors_store) + self.sstatemirrors_tv.set_cursor(self.selected_mirror_row) + hbox.pack_start(self.scroll, expand=True, fill=True) + hbox.show_all() + + return hbox, sstatemirrors_store + + def drag_data_get_cb(self, treeview, context, selection, target_id, etime): + treeselection = treeview.get_selection() + model, iter = treeselection.get_selected() + data = model.get_string_from_iter(iter) + selection.set(selection.target, 8, data) + + def drag_data_received_cb(self, treeview, context, x, y, selection, info, etime): + model = treeview.get_model() + data = [] + tree_iter = model.get_iter_from_string(selection.data) + data.append(model.get_value(tree_iter, 0)) + data.append(model.get_value(tree_iter, 1)) + data.append(model.get_value(tree_iter, 2)) + + drop_info = treeview.get_dest_row_at_pos(x, y) + if drop_info: + path, position = drop_info + iter = model.get_iter(path) + if (position == gtk.TREE_VIEW_DROP_BEFORE or position == gtk.TREE_VIEW_DROP_INTO_OR_BEFORE): + model.insert_before(iter, data) + else: + model.insert_after(iter, data) + else: + model.append(data) + if context.action == gtk.gdk.ACTION_MOVE: + context.finish(True, True, etime) + return + + def delete_cb(self, button): + selection = self.sstatemirrors_tv.get_selection() + tree_model, tree_iter = selection.get_selected() + index = int(tree_model.get_string_from_iter(tree_iter)) + if index == 0: + self.selected_mirror_row = index + else: + self.selected_mirror_row = index - 1 + self.sstatemirrors_list.pop(index) + self.refresh_shared_state_page() + if not self.sstatemirrors_list: + self.delete_button.set_sensitive(False) + + def add_mirror(self, button): + self.new_mirror = True + tooltip = "Select the pre-built mirror that will speed your build" + index = len(self.sstatemirrors_list) + self.selected_mirror_row = index + sm_list = ["Standard", "", "file://(.*)"] + self.sstatemirrors_list.append(sm_list) + self.refresh_shared_state_page() + + def scroll_changed(self, widget, event, data=None): + if self.new_mirror == True: + adj = widget.get_vadjustment() + adj.set_value(adj.upper - adj.page_size) + self.new_mirror = False + + def combo_changed(self, widget, path, text, model): + model[path][0] = text + selection = self.sstatemirrors_tv.get_selection() + tree_model, tree_iter = selection.get_selected() + index = int(tree_model.get_string_from_iter(tree_iter)) + self.sstatemirrors_list[index][0] = text + + def regex_changed(self, cell, path, new_text, user_data): + user_data[path][2] = new_text + selection = self.sstatemirrors_tv.get_selection() + tree_model, tree_iter = selection.get_selected() + index = int(tree_model.get_string_from_iter(tree_iter)) + self.sstatemirrors_list[index][2] = new_text + return + + def url_changed(self, cell, path, new_text, user_data): + if new_text!="Enter the mirror URL" and new_text!="Match regex and replace it with this URL": + user_data[path][1] = new_text + selection = self.sstatemirrors_tv.get_selection() + tree_model, tree_iter = selection.get_selected() + index = int(tree_model.get_string_from_iter(tree_iter)) + self.sstatemirrors_list[index][1] = new_text + return + + def configuration_field(self, column, cell, model, iter): + cell.set_property('text', model.get_value(iter, 0)) + if model.get_value(iter, 0) == "Standard": + self.cell1.set_property("sensitive", False) + self.cell1.set_property("editable", False) + else: + self.cell1.set_property("sensitive", True) + self.cell1.set_property("editable", True) + return + + def regex_field(self, column, cell, model, iter): + cell.set_property('text', model.get_value(iter, 2)) + return + + def url_field(self, column, cell, model, iter): + text = model.get_value(iter, 1) + if text == "": + if model.get_value(iter, 0) == "Standard": + text = "Enter the mirror URL" + else: + text = "Match regex and replace it with this URL" + cell.set_property('text', text) + return + + def refresh_shared_state_page(self): + page_num = self.nb.get_current_page() + self.nb.remove_page(page_num); + self.nb.insert_page(self.create_shared_state_page(), gtk.Label("Shared state"),page_num) + self.show_all() + self.nb.set_current_page(page_num) + + def test_proxy_ended(self, passed): + self.proxy_test_running = False + self.set_test_proxy_state(self.TEST_NETWORK_PASSED if passed else self.TEST_NETWORK_FAILED) + self.set_sensitive(True) + self.refresh_proxy_components() + + def timer_func(self): + self.test_proxy_progress.pulse() + return self.proxy_test_running + + def test_network_button_cb(self, b): + self.set_test_proxy_state(self.TEST_NETWORK_RUNNING) + self.set_sensitive(False) + self.save_proxy_data() + if self.configuration.enable_proxy == True: + self.handler.set_http_proxy(self.configuration.combine_proxy("http")) + self.handler.set_https_proxy(self.configuration.combine_proxy("https")) + self.handler.set_ftp_proxy(self.configuration.combine_proxy("ftp")) + self.handler.set_socks_proxy(self.configuration.combine_proxy("socks")) + self.handler.set_cvs_proxy(self.configuration.combine_host_only("cvs"), self.configuration.combine_port_only("cvs")) + elif self.configuration.enable_proxy == False: + self.handler.set_http_proxy("") + self.handler.set_https_proxy("") + self.handler.set_ftp_proxy("") + self.handler.set_socks_proxy("") + self.handler.set_cvs_proxy("", "") + self.proxy_test_ran = True + self.proxy_test_running = True + gobject.timeout_add(100, self.timer_func) + self.handler.trigger_network_test() + + def test_proxy_focus_event(self, w, direction): + if self.test_proxy_state in [self.TEST_NETWORK_PASSED, self.TEST_NETWORK_FAILED]: + self.set_test_proxy_state(self.TEST_NETWORK_INITIAL) + return False + + def http_proxy_changed(self, e): + if not self.configuration.same_proxy: + return + if e == self.http_proxy: + [w.set_text(self.http_proxy.get_text()) for w in self.same_proxy_addresses] + else: + [w.set_text(self.http_proxy_port.get_text()) for w in self.same_proxy_ports] + + def proxy_address_focus_out_event(self, w, direction): + text = w.get_text() + if not text: + return False + if text.find("//") == -1: + w.set_text("http://" + text) + return False + + def set_test_proxy_state(self, state): + if self.test_proxy_state == state: + return + [self.proxy_table.remove(w) for w in self.test_gui_elements] + if state == self.TEST_NETWORK_INITIAL: + self.proxy_table.attach(self.test_network_button, 1, 2, 5, 6) + self.test_network_button.show() + elif state == self.TEST_NETWORK_RUNNING: + self.test_proxy_progress.set_rcstyle("running") + self.test_proxy_progress.set_text("Testing network configuration") + self.proxy_table.attach(self.test_proxy_progress, 0, 5, 5, 6, xpadding=4) + self.test_proxy_progress.show() + else: # passed or failed + self.dummy_progress.update(1.0) + if state == self.TEST_NETWORK_PASSED: + self.dummy_progress.set_text("Your network is properly configured") + self.dummy_progress.set_rcstyle("running") + else: + self.dummy_progress.set_text("Network test failed") + self.dummy_progress.set_rcstyle("fail") + self.proxy_table.attach(self.dummy_progress, 0, 4, 5, 6) + self.proxy_table.attach(self.retest_network_button, 4, 5, 5, 6, xpadding=4) + self.dummy_progress.show() + self.retest_network_button.show() + self.test_proxy_state = state + + def create_network_page(self): + advanced_vbox = gtk.VBox(False, 6) + advanced_vbox.set_border_width(6) + self.same_proxy_addresses = [] + self.same_proxy_ports = [] + self.all_proxy_ports = [] + self.all_proxy_addresses = [] + + sub_vbox = gtk.VBox(False, 6) + advanced_vbox.pack_start(sub_vbox, expand=False, fill=False) + label = self.gen_label_widget("Set the proxies used when fetching source code") + tooltip = "Set the proxies used when fetching source code. A blank field uses a direct internet connection." + info = HobInfoButton("Set the proxies used when fetching source code" + "*" + tooltip, self) + hbox = gtk.HBox(False, 12) + hbox.pack_start(label, expand=True, fill=True) + hbox.pack_start(info, expand=False, fill=False) + sub_vbox.pack_start(hbox, expand=False, fill=False) + + proxy_test_focus = [] + self.direct_checkbox = gtk.RadioButton(None, "Direct network connection") + proxy_test_focus.append(self.direct_checkbox) + self.direct_checkbox.set_tooltip_text("Check this box to use a direct internet connection with no proxy") + self.direct_checkbox.set_active(not self.configuration.enable_proxy) + sub_vbox.pack_start(self.direct_checkbox, expand=False, fill=False) + + self.proxy_checkbox = gtk.RadioButton(self.direct_checkbox, "Manual proxy configuration") + proxy_test_focus.append(self.proxy_checkbox) + self.proxy_checkbox.set_tooltip_text("Check this box to manually set up a specific proxy") + self.proxy_checkbox.set_active(self.configuration.enable_proxy) + sub_vbox.pack_start(self.proxy_checkbox, expand=False, fill=False) + + self.same_checkbox = gtk.CheckButton("Use the HTTP proxy for all protocols") + proxy_test_focus.append(self.same_checkbox) + self.same_checkbox.set_tooltip_text("Check this box to use the HTTP proxy for all five proxies") + self.same_checkbox.set_active(self.configuration.same_proxy) + hbox = gtk.HBox(False, 12) + hbox.pack_start(self.same_checkbox, expand=False, fill=False, padding=24) + sub_vbox.pack_start(hbox, expand=False, fill=False) + + self.proxy_table = gtk.Table(6, 5, False) + self.http_proxy, self.http_proxy_port, self.http_proxy_details = self.gen_proxy_entry_widget( + "http", self, True, 0) + proxy_test_focus +=[self.http_proxy, self.http_proxy_port] + self.http_proxy.connect("changed", self.http_proxy_changed) + self.http_proxy_port.connect("changed", self.http_proxy_changed) + + self.https_proxy, self.https_proxy_port, self.https_proxy_details = self.gen_proxy_entry_widget( + "https", self, True, 1) + proxy_test_focus += [self.https_proxy, self.https_proxy_port] + self.same_proxy_addresses.append(self.https_proxy) + self.same_proxy_ports.append(self.https_proxy_port) + + self.ftp_proxy, self.ftp_proxy_port, self.ftp_proxy_details = self.gen_proxy_entry_widget( + "ftp", self, True, 2) + proxy_test_focus += [self.ftp_proxy, self.ftp_proxy_port] + self.same_proxy_addresses.append(self.ftp_proxy) + self.same_proxy_ports.append(self.ftp_proxy_port) + + self.socks_proxy, self.socks_proxy_port, self.socks_proxy_details = self.gen_proxy_entry_widget( + "socks", self, True, 3) + proxy_test_focus += [self.socks_proxy, self.socks_proxy_port] + self.same_proxy_addresses.append(self.socks_proxy) + self.same_proxy_ports.append(self.socks_proxy_port) + + self.cvs_proxy, self.cvs_proxy_port, self.cvs_proxy_details = self.gen_proxy_entry_widget( + "cvs", self, True, 4) + proxy_test_focus += [self.cvs_proxy, self.cvs_proxy_port] + self.same_proxy_addresses.append(self.cvs_proxy) + self.same_proxy_ports.append(self.cvs_proxy_port) + self.all_proxy_ports = self.same_proxy_ports + [self.http_proxy_port] + self.all_proxy_addresses = self.same_proxy_addresses + [self.http_proxy] + sub_vbox.pack_start(self.proxy_table, expand=False, fill=False) + self.proxy_table.show_all() + + # Create the graphical elements for the network test feature, but don't display them yet + self.test_network_button = HobAltButton("Test network configuration") + self.test_network_button.connect("clicked", self.test_network_button_cb) + self.test_proxy_progress = HobProgressBar() + self.dummy_progress = HobProgressBar() + self.retest_network_button = HobAltButton("Retest") + self.retest_network_button.connect("clicked", self.test_network_button_cb) + self.test_gui_elements = [self.test_network_button, self.test_proxy_progress, self.dummy_progress, self.retest_network_button] + # Initialize the network tester + self.test_proxy_state = self.TEST_NETWORK_NONE + self.set_test_proxy_state(self.TEST_NETWORK_INITIAL) + self.proxy_test_passed_id = self.handler.connect("network-passed", lambda h:self.test_proxy_ended(True)) + self.proxy_test_failed_id = self.handler.connect("network-failed", lambda h:self.test_proxy_ended(False)) + [w.connect("focus-in-event", self.test_proxy_focus_event) for w in proxy_test_focus] + [w.connect("focus-out-event", self.proxy_address_focus_out_event) for w in self.all_proxy_addresses] + + self.direct_checkbox.connect("toggled", self.proxy_checkbox_toggled_cb) + self.proxy_checkbox.connect("toggled", self.proxy_checkbox_toggled_cb) + self.same_checkbox.connect("toggled", self.same_checkbox_toggled_cb) + + self.refresh_proxy_components() + return advanced_vbox + + def switch_to_page(self, page_id): + self.nb.set_current_page(page_id) + + def details_cb(self, button, parent, protocol): + self.save_proxy_data() + dialog = ProxyDetailsDialog(title = protocol.upper() + " Proxy Details", + user = self.configuration.proxies[protocol][1], + passwd = self.configuration.proxies[protocol][2], + parent = parent, + flags = gtk.DIALOG_MODAL + | gtk.DIALOG_DESTROY_WITH_PARENT + | gtk.DIALOG_NO_SEPARATOR) + dialog.add_button(gtk.STOCK_CLOSE, gtk.RESPONSE_OK) + response = dialog.run() + if response == gtk.RESPONSE_OK: + self.configuration.proxies[protocol][1] = dialog.user + self.configuration.proxies[protocol][2] = dialog.passwd + self.refresh_proxy_components() + dialog.destroy() + + def rootfs_combo_changed_cb(self, rootfs_combo, all_package_format, check_hbox): + combo_item = self.rootfs_combo.get_active_text() + for child in check_hbox.get_children(): + if isinstance(child, gtk.CheckButton): + check_hbox.remove(child) + for format in all_package_format: + if format != combo_item: + check_button = gtk.CheckButton(format) + check_hbox.pack_start(check_button, expand=False, fill=False) + check_hbox.show_all() + + def gen_pkgfmt_widget(self, curr_package_format, all_package_format, tooltip_combo="", tooltip_extra=""): + pkgfmt_hbox = gtk.HBox(False, 24) + + rootfs_vbox = gtk.VBox(False, 6) + pkgfmt_hbox.pack_start(rootfs_vbox, expand=False, fill=False) + + label = self.gen_label_widget("Root file system package format") + rootfs_vbox.pack_start(label, expand=False, fill=False) + + rootfs_format = "" + if curr_package_format: + rootfs_format = curr_package_format.split()[0] + + rootfs_format_widget, rootfs_combo = self.gen_combo_widget(rootfs_format, all_package_format, tooltip_combo) + rootfs_vbox.pack_start(rootfs_format_widget, expand=False, fill=False) + + extra_vbox = gtk.VBox(False, 6) + pkgfmt_hbox.pack_start(extra_vbox, expand=False, fill=False) + + label = self.gen_label_widget("Additional package formats") + extra_vbox.pack_start(label, expand=False, fill=False) + + check_hbox = gtk.HBox(False, 12) + extra_vbox.pack_start(check_hbox, expand=False, fill=False) + for format in all_package_format: + if format != rootfs_format: + check_button = gtk.CheckButton(format) + is_active = (format in curr_package_format.split()) + check_button.set_active(is_active) + check_hbox.pack_start(check_button, expand=False, fill=False) + + info = HobInfoButton(tooltip_extra, self) + check_hbox.pack_end(info, expand=False, fill=False) + + rootfs_combo.connect("changed", self.rootfs_combo_changed_cb, all_package_format, check_hbox) + + pkgfmt_hbox.show_all() + + return pkgfmt_hbox, rootfs_combo, check_hbox + + def editable_settings_cell_edited(self, cell, path_string, new_text, model): + it = model.get_iter_from_string(path_string) + column = cell.get_data("column") + model.set(it, column, new_text) + + def editable_settings_add_item_clicked(self, button, model): + new_item = ["##KEY##", "##VALUE##"] + + iter = model.append() + model.set (iter, + 0, new_item[0], + 1, new_item[1], + ) + + def editable_settings_remove_item_clicked(self, button, treeview): + selection = treeview.get_selection() + model, iter = selection.get_selected() + + if iter: + path = model.get_path(iter)[0] + model.remove(iter) + + def gen_editable_settings(self, setting, tooltip=""): + setting_hbox = gtk.HBox(False, 12) + + vbox = gtk.VBox(False, 12) + setting_hbox.pack_start(vbox, expand=True, fill=True) + + setting_store = gtk.ListStore(gobject.TYPE_STRING, gobject.TYPE_STRING) + for key in setting.keys(): + setting_store.set(setting_store.append(), 0, key, 1, setting[key]) + + setting_tree = gtk.TreeView(setting_store) + setting_tree.set_headers_visible(True) + setting_tree.set_size_request(300, 100) + + col = gtk.TreeViewColumn('Key') + col.set_min_width(100) + col.set_max_width(150) + col.set_resizable(True) + col1 = gtk.TreeViewColumn('Value') + col1.set_min_width(100) + col1.set_max_width(150) + col1.set_resizable(True) + setting_tree.append_column(col) + setting_tree.append_column(col1) + cell = gtk.CellRendererText() + cell.set_property('width-chars', 10) + cell.set_property('editable', True) + cell.set_data("column", 0) + cell.connect("edited", self.editable_settings_cell_edited, setting_store) + cell1 = gtk.CellRendererText() + cell1.set_property('width-chars', 10) + cell1.set_property('editable', True) + cell1.set_data("column", 1) + cell1.connect("edited", self.editable_settings_cell_edited, setting_store) + col.pack_start(cell, True) + col1.pack_end(cell1, True) + col.set_attributes(cell, text=0) + col1.set_attributes(cell1, text=1) + + scroll = gtk.ScrolledWindow() + scroll.set_shadow_type(gtk.SHADOW_IN) + scroll.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) + scroll.add(setting_tree) + vbox.pack_start(scroll, expand=True, fill=True) + + # some buttons + hbox = gtk.HBox(True, 6) + vbox.pack_start(hbox, False, False) + + button = gtk.Button(stock=gtk.STOCK_ADD) + button.connect("clicked", self.editable_settings_add_item_clicked, setting_store) + hbox.pack_start(button) + + button = gtk.Button(stock=gtk.STOCK_REMOVE) + button.connect("clicked", self.editable_settings_remove_item_clicked, setting_tree) + hbox.pack_start(button) + + info = HobInfoButton(tooltip, self) + setting_hbox.pack_start(info, expand=False, fill=False) + + return setting_hbox, setting_store + + def create_others_page(self): + advanced_vbox = gtk.VBox(False, 6) + advanced_vbox.set_border_width(6) + + sub_vbox = gtk.VBox(False, 6) + advanced_vbox.pack_start(sub_vbox, expand=True, fill=True) + label = self.gen_label_widget("Add your own variables:") + tooltip = "These are key/value pairs for your extra settings. Click \'Add\' and then directly edit the key and the value" + setting_widget, self.setting_store = self.gen_editable_settings(self.configuration.extra_setting,"Add your own variables" + "*" + tooltip) + sub_vbox.pack_start(label, expand=False, fill=False) + sub_vbox.pack_start(setting_widget, expand=True, fill=True) + + return advanced_vbox + + def create_visual_elements(self): + self.nb = gtk.Notebook() + self.nb.set_show_tabs(True) + self.nb.append_page(self.create_build_environment_page(), gtk.Label("Build environment")) + self.nb.append_page(self.create_shared_state_page(), gtk.Label("Shared state")) + self.nb.append_page(self.create_network_page(), gtk.Label("Network")) + self.nb.append_page(self.create_others_page(), gtk.Label("Others")) + self.nb.set_current_page(0) + self.vbox.pack_start(self.nb, expand=True, fill=True) + self.vbox.pack_end(gtk.HSeparator(), expand=True, fill=True) + + self.show_all() + + def destroy(self): + self.handler.disconnect(self.proxy_test_passed_id) + self.handler.disconnect(self.proxy_test_failed_id) + super(SimpleSettingsDialog, self).destroy() diff --git a/bitbake/lib/bb/ui/crumbs/hobcolor.py b/bitbake/lib/bb/ui/crumbs/hobcolor.py new file mode 100644 index 0000000000..3316542a20 --- /dev/null +++ b/bitbake/lib/bb/ui/crumbs/hobcolor.py @@ -0,0 +1,38 @@ +# +# BitBake Graphical GTK User Interface +# +# Copyright (C) 2012 Intel Corporation +# +# Authored by Shane Wang +# +# 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. + +class HobColors: + WHITE = "#ffffff" + PALE_GREEN = "#aaffaa" + ORANGE = "#eb8e68" + PALE_RED = "#ffaaaa" + GRAY = "#aaaaaa" + LIGHT_GRAY = "#dddddd" + SLIGHT_DARK = "#5f5f5f" + DARK = "#3c3b37" + BLACK = "#000000" + PALE_BLUE = "#53b8ff" + DEEP_RED = "#aa3e3e" + KHAKI = "#fff68f" + + OK = WHITE + RUNNING = PALE_GREEN + WARNING = ORANGE + ERROR = PALE_RED diff --git a/bitbake/lib/bb/ui/crumbs/hobeventhandler.py b/bitbake/lib/bb/ui/crumbs/hobeventhandler.py new file mode 100644 index 0000000000..43edb70b08 --- /dev/null +++ b/bitbake/lib/bb/ui/crumbs/hobeventhandler.py @@ -0,0 +1,639 @@ +# +# BitBake Graphical GTK User Interface +# +# Copyright (C) 2011 Intel Corporation +# +# Authored by Joshua Lock +# Authored by Dongxiao Xu +# +# 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 gobject +import logging +import ast +from bb.ui.crumbs.runningbuild import RunningBuild + +class HobHandler(gobject.GObject): + + """ + This object does BitBake event handling for the hob gui. + """ + __gsignals__ = { + "package-formats-updated" : (gobject.SIGNAL_RUN_LAST, + gobject.TYPE_NONE, + (gobject.TYPE_PYOBJECT,)), + "config-updated" : (gobject.SIGNAL_RUN_LAST, + gobject.TYPE_NONE, + (gobject.TYPE_STRING, gobject.TYPE_PYOBJECT,)), + "command-succeeded" : (gobject.SIGNAL_RUN_LAST, + gobject.TYPE_NONE, + (gobject.TYPE_INT,)), + "command-failed" : (gobject.SIGNAL_RUN_LAST, + gobject.TYPE_NONE, + (gobject.TYPE_STRING,)), + "parsing-warning" : (gobject.SIGNAL_RUN_LAST, + gobject.TYPE_NONE, + (gobject.TYPE_STRING,)), + "sanity-failed" : (gobject.SIGNAL_RUN_LAST, + gobject.TYPE_NONE, + (gobject.TYPE_STRING, gobject.TYPE_INT)), + "generating-data" : (gobject.SIGNAL_RUN_LAST, + gobject.TYPE_NONE, + ()), + "data-generated" : (gobject.SIGNAL_RUN_LAST, + gobject.TYPE_NONE, + ()), + "parsing-started" : (gobject.SIGNAL_RUN_LAST, + gobject.TYPE_NONE, + (gobject.TYPE_PYOBJECT,)), + "parsing" : (gobject.SIGNAL_RUN_LAST, + gobject.TYPE_NONE, + (gobject.TYPE_PYOBJECT,)), + "parsing-completed" : (gobject.SIGNAL_RUN_LAST, + gobject.TYPE_NONE, + (gobject.TYPE_PYOBJECT,)), + "recipe-populated" : (gobject.SIGNAL_RUN_LAST, + gobject.TYPE_NONE, + ()), + "package-populated" : (gobject.SIGNAL_RUN_LAST, + gobject.TYPE_NONE, + ()), + "network-passed" : (gobject.SIGNAL_RUN_LAST, + gobject.TYPE_NONE, + ()), + "network-failed" : (gobject.SIGNAL_RUN_LAST, + gobject.TYPE_NONE, + ()), + } + + (GENERATE_CONFIGURATION, GENERATE_RECIPES, GENERATE_PACKAGES, GENERATE_IMAGE, POPULATE_PACKAGEINFO, SANITY_CHECK, NETWORK_TEST) = range(7) + (SUB_PATH_LAYERS, SUB_FILES_DISTRO, SUB_FILES_MACH, SUB_FILES_SDKMACH, SUB_MATCH_CLASS, SUB_PARSE_CONFIG, SUB_SANITY_CHECK, + SUB_GNERATE_TGTS, SUB_GENERATE_PKGINFO, SUB_BUILD_RECIPES, SUB_BUILD_IMAGE, SUB_NETWORK_TEST) = range(12) + + def __init__(self, server, recipe_model, package_model): + super(HobHandler, self).__init__() + + self.build = RunningBuild(sequential=True) + + self.recipe_model = recipe_model + self.package_model = package_model + + self.commands_async = [] + self.generating = False + self.current_phase = None + self.building = False + self.recipe_queue = [] + self.package_queue = [] + + self.server = server + self.error_msg = "" + self.initcmd = None + self.parsing = False + + def set_busy(self): + if not self.generating: + self.emit("generating-data") + self.generating = True + + def clear_busy(self): + if self.generating: + self.emit("data-generated") + self.generating = False + + def runCommand(self, commandline): + try: + result, error = self.server.runCommand(commandline) + if error: + raise Exception("Error running command '%s': %s" % (commandline, error)) + return result + except Exception as e: + self.commands_async = [] + self.clear_busy() + self.emit("command-failed", "Hob Exception - %s" % (str(e))) + return None + + def run_next_command(self, initcmd=None): + if initcmd != None: + self.initcmd = initcmd + + if self.commands_async: + self.set_busy() + next_command = self.commands_async.pop(0) + else: + self.clear_busy() + if self.initcmd != None: + self.emit("command-succeeded", self.initcmd) + return + + if next_command == self.SUB_PATH_LAYERS: + self.runCommand(["findConfigFilePath", "bblayers.conf"]) + elif next_command == self.SUB_FILES_DISTRO: + self.runCommand(["findConfigFiles", "DISTRO"]) + elif next_command == self.SUB_FILES_MACH: + self.runCommand(["findConfigFiles", "MACHINE"]) + elif next_command == self.SUB_FILES_SDKMACH: + self.runCommand(["findConfigFiles", "MACHINE-SDK"]) + elif next_command == self.SUB_MATCH_CLASS: + self.runCommand(["findFilesMatchingInDir", "rootfs_", "classes"]) + elif next_command == self.SUB_PARSE_CONFIG: + self.runCommand(["resetCooker"]) + elif next_command == self.SUB_GNERATE_TGTS: + self.runCommand(["generateTargetsTree", "classes/image.bbclass", []]) + elif next_command == self.SUB_GENERATE_PKGINFO: + self.runCommand(["triggerEvent", "bb.event.RequestPackageInfo()"]) + elif next_command == self.SUB_SANITY_CHECK: + self.runCommand(["triggerEvent", "bb.event.SanityCheck()"]) + elif next_command == self.SUB_NETWORK_TEST: + self.runCommand(["triggerEvent", "bb.event.NetworkTest()"]) + elif next_command == self.SUB_BUILD_RECIPES: + self.clear_busy() + self.building = True + self.runCommand(["buildTargets", self.recipe_queue, self.default_task]) + self.recipe_queue = [] + elif next_command == self.SUB_BUILD_IMAGE: + self.clear_busy() + self.building = True + target = self.image + + if self.base_image: + # Request the build of a custom image + self.generate_hob_base_image(target) + self.set_var_in_file("LINGUAS_INSTALL", "", "local.conf") + hobImage = self.runCommand(["matchFile", target + ".bb"]) + if self.base_image != self.recipe_model.__custom_image__: + baseImage = self.runCommand(["matchFile", self.base_image + ".bb"]) + version = self.runCommand(["generateNewImage", hobImage, baseImage, self.package_queue, True, ""]) + target += version + self.recipe_model.set_custom_image_version(version) + + targets = [target] + if self.toolchain_packages: + self.set_var_in_file("TOOLCHAIN_TARGET_TASK", " ".join(self.toolchain_packages), "local.conf") + targets.append(target + ":do_populate_sdk") + + self.runCommand(["buildTargets", targets, self.default_task]) + + def display_error(self): + self.clear_busy() + self.emit("command-failed", self.error_msg) + self.error_msg = "" + if self.building: + self.building = False + + def handle_event(self, event): + if not event: + return + if self.building: + self.current_phase = "building" + self.build.handle_event(event) + + if isinstance(event, bb.event.PackageInfo): + self.package_model.populate(event._pkginfolist) + self.emit("package-populated") + self.run_next_command() + + elif isinstance(event, bb.event.SanityCheckPassed): + reparse = self.runCommand(["getVariable", "BB_INVALIDCONF"]) or None + if reparse is True: + self.set_var_in_file("BB_INVALIDCONF", False, "local.conf") + self.runCommand(["setPrePostConfFiles", "conf/.hob.conf", ""]) + self.commands_async.prepend(self.SUB_PARSE_CONFIG) + self.run_next_command() + + elif isinstance(event, bb.event.SanityCheckFailed): + self.emit("sanity-failed", event._msg, event._network_error) + + elif isinstance(event, logging.LogRecord): + if not self.building: + if event.levelno >= logging.ERROR: + formatter = bb.msg.BBLogFormatter() + msg = formatter.format(event) + self.error_msg += msg + '\n' + elif event.levelno >= logging.WARNING and self.parsing == True: + formatter = bb.msg.BBLogFormatter() + msg = formatter.format(event) + warn_msg = msg + '\n' + self.emit("parsing-warning", warn_msg) + + elif isinstance(event, bb.event.TargetsTreeGenerated): + self.current_phase = "data generation" + if event._model: + self.recipe_model.populate(event._model) + self.emit("recipe-populated") + elif isinstance(event, bb.event.ConfigFilesFound): + self.current_phase = "configuration lookup" + var = event._variable + values = event._values + values.sort() + self.emit("config-updated", var, values) + elif isinstance(event, bb.event.ConfigFilePathFound): + self.current_phase = "configuration lookup" + elif isinstance(event, bb.event.FilesMatchingFound): + self.current_phase = "configuration lookup" + # FIXME: hard coding, should at least be a variable shared between + # here and the caller + if event._pattern == "rootfs_": + formats = [] + for match in event._matches: + classname, sep, cls = match.rpartition(".") + fs, sep, format = classname.rpartition("_") + formats.append(format) + formats.sort() + self.emit("package-formats-updated", formats) + elif isinstance(event, bb.command.CommandCompleted): + self.current_phase = None + self.run_next_command() + elif isinstance(event, bb.command.CommandFailed): + if event.error not in ("Forced shutdown", "Stopped build"): + self.error_msg += event.error + self.commands_async = [] + self.display_error() + elif isinstance(event, (bb.event.ParseStarted, + bb.event.CacheLoadStarted, + bb.event.TreeDataPreparationStarted, + )): + message = {} + message["eventname"] = bb.event.getName(event) + message["current"] = 0 + message["total"] = None + message["title"] = "Parsing recipes" + self.emit("parsing-started", message) + if isinstance(event, bb.event.ParseStarted): + self.parsing = True + elif isinstance(event, (bb.event.ParseProgress, + bb.event.CacheLoadProgress, + bb.event.TreeDataPreparationProgress)): + message = {} + message["eventname"] = bb.event.getName(event) + message["current"] = event.current + message["total"] = event.total + message["title"] = "Parsing recipes" + self.emit("parsing", message) + elif isinstance(event, (bb.event.ParseCompleted, + bb.event.CacheLoadCompleted, + bb.event.TreeDataPreparationCompleted)): + message = {} + message["eventname"] = bb.event.getName(event) + message["current"] = event.total + message["total"] = event.total + message["title"] = "Parsing recipes" + self.emit("parsing-completed", message) + if isinstance(event, bb.event.ParseCompleted): + self.parsing = False + elif isinstance(event, bb.event.NetworkTestFailed): + self.emit("network-failed") + self.run_next_command() + elif isinstance(event, bb.event.NetworkTestPassed): + self.emit("network-passed") + self.run_next_command() + + if self.error_msg and not self.commands_async: + self.display_error() + + return + + def init_cooker(self): + self.runCommand(["createConfigFile", ".hob.conf"]) + + def set_extra_inherit(self, bbclass): + self.append_var_in_file("INHERIT", bbclass, ".hob.conf") + + def set_bblayers(self, bblayers): + self.set_var_in_file("BBLAYERS", " ".join(bblayers), "bblayers.conf") + + def set_machine(self, machine): + if machine: + self.early_assign_var_in_file("MACHINE", machine, "local.conf") + + def set_sdk_machine(self, sdk_machine): + self.set_var_in_file("SDKMACHINE", sdk_machine, "local.conf") + + def set_image_fstypes(self, image_fstypes): + self.set_var_in_file("IMAGE_FSTYPES", image_fstypes, "local.conf") + + def set_distro(self, distro): + self.set_var_in_file("DISTRO", distro, "local.conf") + + def set_package_format(self, format): + package_classes = "" + for pkgfmt in format.split(): + package_classes += ("package_%s" % pkgfmt + " ") + self.set_var_in_file("PACKAGE_CLASSES", package_classes, "local.conf") + + def set_bbthreads(self, threads): + self.set_var_in_file("BB_NUMBER_THREADS", threads, "local.conf") + + def set_pmake(self, threads): + pmake = "-j %s" % threads + self.set_var_in_file("PARALLEL_MAKE", pmake, "local.conf") + + def set_dl_dir(self, directory): + self.set_var_in_file("DL_DIR", directory, "local.conf") + + def set_sstate_dir(self, directory): + self.set_var_in_file("SSTATE_DIR", directory, "local.conf") + + def set_sstate_mirrors(self, url): + self.set_var_in_file("SSTATE_MIRRORS", url, "local.conf") + + def set_extra_size(self, image_extra_size): + self.set_var_in_file("IMAGE_ROOTFS_EXTRA_SPACE", str(image_extra_size), "local.conf") + + def set_rootfs_size(self, image_rootfs_size): + self.set_var_in_file("IMAGE_ROOTFS_SIZE", str(image_rootfs_size), "local.conf") + + def set_incompatible_license(self, incompat_license): + self.set_var_in_file("INCOMPATIBLE_LICENSE", incompat_license, "local.conf") + + def set_extra_setting(self, extra_setting): + self.set_var_in_file("EXTRA_SETTING", extra_setting, "local.conf") + + def set_extra_config(self, extra_setting): + old_extra_setting = self.runCommand(["getVariable", "EXTRA_SETTING"]) or {} + old_extra_setting = str(old_extra_setting) + + old_extra_setting = ast.literal_eval(old_extra_setting) + if not type(old_extra_setting) == dict: + old_extra_setting = {} + + # settings not changed + if old_extra_setting == extra_setting: + return + + # remove the old EXTRA SETTING variable + self.remove_var_from_file("EXTRA_SETTING") + + # remove old settings from conf + for key in old_extra_setting.keys(): + if key not in extra_setting: + self.remove_var_from_file(key) + + # add new settings + for key, value in extra_setting.iteritems(): + self.set_var_in_file(key, value, "local.conf") + + if extra_setting: + self.set_var_in_file("EXTRA_SETTING", extra_setting, "local.conf") + + def set_http_proxy(self, http_proxy): + self.set_var_in_file("http_proxy", http_proxy, "local.conf") + + def set_https_proxy(self, https_proxy): + self.set_var_in_file("https_proxy", https_proxy, "local.conf") + + def set_ftp_proxy(self, ftp_proxy): + self.set_var_in_file("ftp_proxy", ftp_proxy, "local.conf") + + def set_socks_proxy(self, socks_proxy): + self.set_var_in_file("all_proxy", socks_proxy, "local.conf") + + def set_cvs_proxy(self, host, port): + self.set_var_in_file("CVS_PROXY_HOST", host, "local.conf") + self.set_var_in_file("CVS_PROXY_PORT", port, "local.conf") + + def request_package_info(self): + self.commands_async.append(self.SUB_GENERATE_PKGINFO) + self.run_next_command(self.POPULATE_PACKAGEINFO) + + def trigger_sanity_check(self): + self.commands_async.append(self.SUB_SANITY_CHECK) + self.run_next_command(self.SANITY_CHECK) + + def trigger_network_test(self): + self.commands_async.append(self.SUB_NETWORK_TEST) + self.run_next_command(self.NETWORK_TEST) + + def generate_configuration(self): + self.runCommand(["setPrePostConfFiles", "conf/.hob.conf", ""]) + self.commands_async.append(self.SUB_PARSE_CONFIG) + self.commands_async.append(self.SUB_PATH_LAYERS) + self.commands_async.append(self.SUB_FILES_DISTRO) + self.commands_async.append(self.SUB_FILES_MACH) + self.commands_async.append(self.SUB_FILES_SDKMACH) + self.commands_async.append(self.SUB_MATCH_CLASS) + self.run_next_command(self.GENERATE_CONFIGURATION) + + def generate_recipes(self): + self.runCommand(["setPrePostConfFiles", "conf/.hob.conf", ""]) + self.commands_async.append(self.SUB_PARSE_CONFIG) + self.commands_async.append(self.SUB_GNERATE_TGTS) + self.run_next_command(self.GENERATE_RECIPES) + + def generate_packages(self, tgts, default_task="build"): + targets = [] + targets.extend(tgts) + self.recipe_queue = targets + self.default_task = default_task + self.runCommand(["setPrePostConfFiles", "conf/.hob.conf", ""]) + self.commands_async.append(self.SUB_PARSE_CONFIG) + self.commands_async.append(self.SUB_BUILD_RECIPES) + self.run_next_command(self.GENERATE_PACKAGES) + + def generate_image(self, image, base_image, image_packages=[], toolchain_packages=[], default_task="build"): + self.image = image + self.base_image = base_image + self.package_queue = image_packages + self.toolchain_packages = toolchain_packages + self.default_task = default_task + self.runCommand(["setPrePostConfFiles", "conf/.hob.conf", ""]) + self.commands_async.append(self.SUB_PARSE_CONFIG) + self.commands_async.append(self.SUB_BUILD_IMAGE) + self.run_next_command(self.GENERATE_IMAGE) + + def generate_new_image(self, image, base_image, package_queue, description): + if base_image: + base_image = self.runCommand(["matchFile", self.base_image + ".bb"]) + self.runCommand(["generateNewImage", image, base_image, package_queue, False, description]) + + def generate_hob_base_image(self, hob_image): + image_dir = self.get_topdir() + "/recipes/images/" + recipe_name = hob_image + ".bb" + self.ensure_dir(image_dir) + self.generate_new_image(image_dir + recipe_name, None, [], "") + + def ensure_dir(self, directory): + self.runCommand(["ensureDir", directory]) + + def build_succeeded_async(self): + self.building = False + + def build_failed_async(self): + self.initcmd = None + self.commands_async = [] + self.building = False + + def cancel_parse(self): + self.runCommand(["stateForceShutdown"]) + + def cancel_build(self, force=False): + if force: + # Force the cooker to stop as quickly as possible + self.runCommand(["stateForceShutdown"]) + else: + # Wait for tasks to complete before shutting down, this helps + # leave the workdir in a usable state + self.runCommand(["stateShutdown"]) + + def reset_build(self): + self.build.reset() + + def get_logfile(self): + return self.server.runCommand(["getVariable", "BB_CONSOLELOG"])[0] + + def get_topdir(self): + return self.runCommand(["getVariable", "TOPDIR"]) or "" + + def _remove_redundant(self, string): + ret = [] + for i in string.split(): + if i not in ret: + ret.append(i) + return " ".join(ret) + + def set_var_in_file(self, var, val, default_file=None): + self.runCommand(["enableDataTracking"]) + self.server.runCommand(["setVarFile", var, val, default_file, "set"]) + self.runCommand(["disableDataTracking"]) + + def early_assign_var_in_file(self, var, val, default_file=None): + self.runCommand(["enableDataTracking"]) + self.server.runCommand(["setVarFile", var, val, default_file, "earlyAssign"]) + self.runCommand(["disableDataTracking"]) + + def remove_var_from_file(self, var): + self.server.runCommand(["removeVarFile", var]) + + def append_var_in_file(self, var, val, default_file=None): + self.server.runCommand(["setVarFile", var, val, default_file, "append"]) + + def append_to_bbfiles(self, val): + bbfiles = self.runCommand(["getVariable", "BBFILES", "False"]) or "" + bbfiles = bbfiles.split() + if val not in bbfiles: + self.append_var_in_file("BBFILES", val, "bblayers.conf") + + def get_parameters(self): + # retrieve the parameters from bitbake + params = {} + params["core_base"] = self.runCommand(["getVariable", "COREBASE"]) or "" + params["layer"] = self.runCommand(["getVariable", "BBLAYERS"]) or "" + params["layers_non_removable"] = self.runCommand(["getVariable", "BBLAYERS_NON_REMOVABLE"]) or "" + params["dldir"] = self.runCommand(["getVariable", "DL_DIR"]) or "" + params["machine"] = self.runCommand(["getVariable", "MACHINE"]) or "" + params["distro"] = self.runCommand(["getVariable", "DISTRO"]) or "defaultsetup" + params["pclass"] = self.runCommand(["getVariable", "PACKAGE_CLASSES"]) or "" + params["sstatedir"] = self.runCommand(["getVariable", "SSTATE_DIR"]) or "" + params["sstatemirror"] = self.runCommand(["getVariable", "SSTATE_MIRRORS"]) or "" + + num_threads = self.runCommand(["getCpuCount"]) + if not num_threads: + num_threads = 1 + max_threads = 65536 + else: + try: + num_threads = int(num_threads) + max_threads = 16 * num_threads + except: + num_threads = 1 + max_threads = 65536 + params["max_threads"] = max_threads + + bbthread = self.runCommand(["getVariable", "BB_NUMBER_THREADS"]) + if not bbthread: + bbthread = num_threads + else: + try: + bbthread = int(bbthread) + except: + bbthread = num_threads + params["bbthread"] = bbthread + + pmake = self.runCommand(["getVariable", "PARALLEL_MAKE"]) + if not pmake: + pmake = num_threads + elif isinstance(pmake, int): + pass + else: + try: + pmake = int(pmake.lstrip("-j ")) + except: + pmake = num_threads + params["pmake"] = "-j %s" % pmake + + params["image_addr"] = self.runCommand(["getVariable", "DEPLOY_DIR_IMAGE"]) or "" + + image_extra_size = self.runCommand(["getVariable", "IMAGE_ROOTFS_EXTRA_SPACE"]) + if not image_extra_size: + image_extra_size = 0 + else: + try: + image_extra_size = int(image_extra_size) + except: + image_extra_size = 0 + params["image_extra_size"] = image_extra_size + + image_rootfs_size = self.runCommand(["getVariable", "IMAGE_ROOTFS_SIZE"]) + if not image_rootfs_size: + image_rootfs_size = 0 + else: + try: + image_rootfs_size = int(image_rootfs_size) + except: + image_rootfs_size = 0 + params["image_rootfs_size"] = image_rootfs_size + + image_overhead_factor = self.runCommand(["getVariable", "IMAGE_OVERHEAD_FACTOR"]) + if not image_overhead_factor: + image_overhead_factor = 1 + else: + try: + image_overhead_factor = float(image_overhead_factor) + except: + image_overhead_factor = 1 + params['image_overhead_factor'] = image_overhead_factor + + params["incompat_license"] = self._remove_redundant(self.runCommand(["getVariable", "INCOMPATIBLE_LICENSE"]) or "") + params["sdk_machine"] = self.runCommand(["getVariable", "SDKMACHINE"]) or self.runCommand(["getVariable", "SDK_ARCH"]) or "" + + params["image_fstypes"] = self._remove_redundant(self.runCommand(["getVariable", "IMAGE_FSTYPES"]) or "") + + params["image_types"] = self._remove_redundant(self.runCommand(["getVariable", "IMAGE_TYPES"]) or "") + + params["conf_version"] = self.runCommand(["getVariable", "CONF_VERSION"]) or "" + params["lconf_version"] = self.runCommand(["getVariable", "LCONF_VERSION"]) or "" + + params["runnable_image_types"] = self._remove_redundant(self.runCommand(["getVariable", "RUNNABLE_IMAGE_TYPES"]) or "") + params["runnable_machine_patterns"] = self._remove_redundant(self.runCommand(["getVariable", "RUNNABLE_MACHINE_PATTERNS"]) or "") + params["deployable_image_types"] = self._remove_redundant(self.runCommand(["getVariable", "DEPLOYABLE_IMAGE_TYPES"]) or "") + params["kernel_image_type"] = self.runCommand(["getVariable", "KERNEL_IMAGETYPE"]) or "" + params["tmpdir"] = self.runCommand(["getVariable", "TMPDIR"]) or "" + params["distro_version"] = self.runCommand(["getVariable", "DISTRO_VERSION"]) or "" + params["target_os"] = self.runCommand(["getVariable", "TARGET_OS"]) or "" + params["target_arch"] = self.runCommand(["getVariable", "TARGET_ARCH"]) or "" + params["tune_pkgarch"] = self.runCommand(["getVariable", "TUNE_PKGARCH"]) or "" + params["bb_version"] = self.runCommand(["getVariable", "BB_MIN_VERSION"]) or "" + + params["default_task"] = self.runCommand(["getVariable", "BB_DEFAULT_TASK"]) or "build" + + params["socks_proxy"] = self.runCommand(["getVariable", "all_proxy"]) or "" + params["http_proxy"] = self.runCommand(["getVariable", "http_proxy"]) or "" + params["ftp_proxy"] = self.runCommand(["getVariable", "ftp_proxy"]) or "" + params["https_proxy"] = self.runCommand(["getVariable", "https_proxy"]) or "" + + params["cvs_proxy_host"] = self.runCommand(["getVariable", "CVS_PROXY_HOST"]) or "" + params["cvs_proxy_port"] = self.runCommand(["getVariable", "CVS_PROXY_PORT"]) or "" + + params["image_white_pattern"] = self.runCommand(["getVariable", "BBUI_IMAGE_WHITE_PATTERN"]) or "" + params["image_black_pattern"] = self.runCommand(["getVariable", "BBUI_IMAGE_BLACK_PATTERN"]) or "" + return params diff --git a/bitbake/lib/bb/ui/crumbs/hoblistmodel.py b/bitbake/lib/bb/ui/crumbs/hoblistmodel.py new file mode 100644 index 0000000000..50df156f4d --- /dev/null +++ b/bitbake/lib/bb/ui/crumbs/hoblistmodel.py @@ -0,0 +1,903 @@ +# +# BitBake Graphical GTK User Interface +# +# Copyright (C) 2011 Intel Corporation +# +# Authored by Joshua Lock +# Authored by Dongxiao Xu +# Authored by Shane Wang +# +# 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 +from bb.ui.crumbs.hobpages import HobPage + +# +# PackageListModel +# +class PackageListModel(gtk.ListStore): + """ + This class defines an gtk.ListStore subclass which will convert the output + of the bb.event.TargetsTreeGenerated event into a gtk.ListStore whilst also + providing convenience functions to access gtk.TreeModel subclasses which + provide filtered views of the data. + """ + + (COL_NAME, COL_VER, COL_REV, COL_RNM, COL_SEC, COL_SUM, COL_RDEP, COL_RPROV, COL_SIZE, COL_RCP, COL_BINB, COL_INC, COL_FADE_INC, COL_FONT, COL_FLIST) = range(15) + + __gsignals__ = { + "package-selection-changed" : (gobject.SIGNAL_RUN_LAST, + gobject.TYPE_NONE, + ()), + } + + __toolchain_required_packages__ = ["packagegroup-core-standalone-sdk-target", "packagegroup-core-standalone-sdk-target-dbg"] + + def __init__(self): + self.rprov_pkg = {} + gtk.ListStore.__init__ (self, + gobject.TYPE_STRING, + gobject.TYPE_STRING, + gobject.TYPE_STRING, + gobject.TYPE_STRING, + gobject.TYPE_STRING, + gobject.TYPE_STRING, + gobject.TYPE_STRING, + gobject.TYPE_STRING, + gobject.TYPE_STRING, + gobject.TYPE_STRING, + gobject.TYPE_STRING, + gobject.TYPE_BOOLEAN, + gobject.TYPE_BOOLEAN, + gobject.TYPE_STRING, + gobject.TYPE_STRING) + self.sort_column_id, self.sort_order = PackageListModel.COL_NAME, gtk.SORT_ASCENDING + + """ + Find the model path for the item_name + Returns the path in the model or None + """ + def find_path_for_item(self, item_name): + pkg = item_name + if item_name not in self.pn_path.keys(): + if item_name not in self.rprov_pkg.keys(): + return None + pkg = self.rprov_pkg[item_name] + if pkg not in self.pn_path.keys(): + return None + + return self.pn_path[pkg] + + def find_item_for_path(self, item_path): + return self[item_path][self.COL_NAME] + + """ + Helper function to determine whether an item is an item specified by filter + """ + def tree_model_filter(self, model, it, filter): + name = model.get_value(it, self.COL_NAME) + + for key in filter.keys(): + if key == self.COL_NAME: + if filter[key] != 'Search packages by name': + if name and filter[key] not in name: + return False + else: + if model.get_value(it, key) not in filter[key]: + return False + self.filtered_nb += 1 + return True + + """ + Create, if required, and return a filtered gtk.TreeModelSort + containing only the items specified by filter + """ + def tree_model(self, filter, excluded_items_ahead=False, included_items_ahead=False, search_data=None, initial=False): + model = self.filter_new() + self.filtered_nb = 0 + model.set_visible_func(self.tree_model_filter, filter) + + sort = gtk.TreeModelSort(model) + sort.connect ('sort-column-changed', self.sort_column_changed_cb) + if initial: + sort.set_sort_column_id(PackageListModel.COL_NAME, gtk.SORT_ASCENDING) + sort.set_default_sort_func(None) + elif excluded_items_ahead: + sort.set_default_sort_func(self.exclude_item_sort_func, search_data) + elif included_items_ahead: + sort.set_default_sort_func(self.include_item_sort_func, search_data) + else: + if search_data and search_data!='Search recipes by name' and search_data!='Search package groups by name': + sort.set_default_sort_func(self.sort_func, search_data) + else: + sort.set_sort_column_id(self.sort_column_id, self.sort_order) + sort.set_default_sort_func(None) + + sort.set_sort_func(PackageListModel.COL_INC, self.sort_column, PackageListModel.COL_INC) + sort.set_sort_func(PackageListModel.COL_SIZE, self.sort_column, PackageListModel.COL_SIZE) + sort.set_sort_func(PackageListModel.COL_BINB, self.sort_binb_column) + sort.set_sort_func(PackageListModel.COL_RCP, self.sort_column, PackageListModel.COL_RCP) + return sort + + def sort_column_changed_cb (self, data): + self.sort_column_id, self.sort_order = data.get_sort_column_id () + + def sort_column(self, model, row1, row2, col): + value1 = model.get_value(row1, col) + value2 = model.get_value(row2, col) + if col==PackageListModel.COL_SIZE: + value1 = HobPage._string_to_size(value1) + value2 = HobPage._string_to_size(value2) + + cmp_res = cmp(value1, value2) + if cmp_res!=0: + if col==PackageListModel.COL_INC: + return -cmp_res + else: + return cmp_res + else: + name1 = model.get_value(row1, PackageListModel.COL_NAME) + name2 = model.get_value(row2, PackageListModel.COL_NAME) + return cmp(name1,name2) + + def sort_binb_column(self, model, row1, row2): + value1 = model.get_value(row1, PackageListModel.COL_BINB) + value2 = model.get_value(row2, PackageListModel.COL_BINB) + value1_list = value1.split(', ') + value2_list = value2.split(', ') + + value1 = value1_list[0] + value2 = value2_list[0] + + cmp_res = cmp(value1, value2) + if cmp_res==0: + cmp_size = cmp(len(value1_list), len(value2_list)) + if cmp_size==0: + name1 = model.get_value(row1, PackageListModel.COL_NAME) + name2 = model.get_value(row2, PackageListModel.COL_NAME) + return cmp(name1,name2) + else: + return cmp_size + else: + return cmp_res + + def exclude_item_sort_func(self, model, iter1, iter2, user_data=None): + if user_data: + val1 = model.get_value(iter1, PackageListModel.COL_NAME) + val2 = model.get_value(iter2, PackageListModel.COL_NAME) + return self.cmp_vals(val1, val2, user_data) + else: + val1 = model.get_value(iter1, PackageListModel.COL_FADE_INC) + val2 = model.get_value(iter2, PackageListModel.COL_INC) + return ((val1 == True) and (val2 == False)) + + def include_item_sort_func(self, model, iter1, iter2, user_data=None): + if user_data: + val1 = model.get_value(iter1, PackageListModel.COL_NAME) + val2 = model.get_value(iter2, PackageListModel.COL_NAME) + return self.cmp_vals(val1, val2, user_data) + else: + val1 = model.get_value(iter1, PackageListModel.COL_INC) + val2 = model.get_value(iter2, PackageListModel.COL_INC) + return ((val1 == False) and (val2 == True)) + + def sort_func(self, model, iter1, iter2, user_data): + val1 = model.get_value(iter1, PackageListModel.COL_NAME) + val2 = model.get_value(iter2, PackageListModel.COL_NAME) + return self.cmp_vals(val1, val2, user_data) + + def cmp_vals(self, val1, val2, user_data): + if val1 is None or val2 is None: + return 0 + elif val1.startswith(user_data) and not val2.startswith(user_data): + return -1 + elif not val1.startswith(user_data) and val2.startswith(user_data): + return 1 + else: + return cmp(val1, val2) + + def convert_vpath_to_path(self, view_model, view_path): + # view_model is the model sorted + # get the path of the model filtered + filtered_model_path = view_model.convert_path_to_child_path(view_path) + # get the model filtered + filtered_model = view_model.get_model() + # get the path of the original model + path = filtered_model.convert_path_to_child_path(filtered_model_path) + return path + + def convert_path_to_vpath(self, view_model, path): + it = view_model.get_iter_first() + while it: + name = self.find_item_for_path(path) + view_name = view_model.get_value(it, PackageListModel.COL_NAME) + if view_name == name: + view_path = view_model.get_path(it) + return view_path + it = view_model.iter_next(it) + return None + + """ + The populate() function takes as input the data from a + bb.event.PackageInfo event and populates the package list. + """ + def populate(self, pkginfolist): + # First clear the model, in case repopulating + self.clear() + + def getpkgvalue(pkgdict, key, pkgname, defaultval = None): + value = pkgdict.get('%s_%s' % (key, pkgname), None) + if not value: + value = pkgdict.get(key, defaultval) + return value + + for pkginfo in pkginfolist: + pn = pkginfo['PN'] + pv = pkginfo['PV'] + pr = pkginfo['PR'] + pkg = pkginfo['PKG'] + pkgv = getpkgvalue(pkginfo, 'PKGV', pkg) + pkgr = getpkgvalue(pkginfo, 'PKGR', pkg) + # PKGSIZE is artificial, will always be overridden with the package name if present + pkgsize = int(pkginfo.get('PKGSIZE_%s' % pkg, "0")) + # PKG_%s is the renamed version + pkg_rename = pkginfo.get('PKG_%s' % pkg, "") + # The rest may be overridden or not + section = getpkgvalue(pkginfo, 'SECTION', pkg, "") + summary = getpkgvalue(pkginfo, 'SUMMARY', pkg, "") + rdep = getpkgvalue(pkginfo, 'RDEPENDS', pkg, "") + rrec = getpkgvalue(pkginfo, 'RRECOMMENDS', pkg, "") + rprov = getpkgvalue(pkginfo, 'RPROVIDES', pkg, "") + files_list = getpkgvalue(pkginfo, 'FILES_INFO', pkg, "") + for i in rprov.split(): + self.rprov_pkg[i] = pkg + + recipe = pn + '-' + pv + '-' + pr + + allow_empty = getpkgvalue(pkginfo, 'ALLOW_EMPTY', pkg, "") + + if pkgsize == 0 and not allow_empty: + continue + + size = HobPage._size_to_string(pkgsize) + self.set(self.append(), self.COL_NAME, pkg, self.COL_VER, pkgv, + self.COL_REV, pkgr, self.COL_RNM, pkg_rename, + self.COL_SEC, section, self.COL_SUM, summary, + self.COL_RDEP, rdep + ' ' + rrec, + self.COL_RPROV, rprov, self.COL_SIZE, size, + self.COL_RCP, recipe, self.COL_BINB, "", + self.COL_INC, False, self.COL_FONT, '10', self.COL_FLIST, files_list) + + self.pn_path = {} + it = self.get_iter_first() + while it: + pn = self.get_value(it, self.COL_NAME) + path = self.get_path(it) + self.pn_path[pn] = path + it = self.iter_next(it) + + """ + Update the model, send out the notification. + """ + def selection_change_notification(self): + self.emit("package-selection-changed") + + """ + Check whether the item at item_path is included or not + """ + def path_included(self, item_path): + return self[item_path][self.COL_INC] + + """ + Add this item, and any of its dependencies, to the image contents + """ + def include_item(self, item_path, binb=""): + if self.path_included(item_path): + return + + item_name = self[item_path][self.COL_NAME] + item_deps = self[item_path][self.COL_RDEP] + + self[item_path][self.COL_INC] = True + + item_bin = self[item_path][self.COL_BINB].split(', ') + if binb and not binb in item_bin: + item_bin.append(binb) + self[item_path][self.COL_BINB] = ', '.join(item_bin).lstrip(', ') + + if item_deps: + # Ensure all of the items deps are included and, where appropriate, + # add this item to their COL_BINB + for dep in item_deps.split(" "): + if dep.startswith('('): + continue + # If the contents model doesn't already contain dep, add it + dep_path = self.find_path_for_item(dep) + if not dep_path: + continue + dep_included = self.path_included(dep_path) + + if dep_included and not dep in item_bin: + # don't set the COL_BINB to this item if the target is an + # item in our own COL_BINB + dep_bin = self[dep_path][self.COL_BINB].split(', ') + if not item_name in dep_bin: + dep_bin.append(item_name) + self[dep_path][self.COL_BINB] = ', '.join(dep_bin).lstrip(', ') + elif not dep_included: + self.include_item(dep_path, binb=item_name) + + def exclude_item(self, item_path): + if not self.path_included(item_path): + return + + self[item_path][self.COL_INC] = False + + item_name = self[item_path][self.COL_NAME] + item_deps = self[item_path][self.COL_RDEP] + if item_deps: + for dep in item_deps.split(" "): + if dep.startswith('('): + continue + dep_path = self.find_path_for_item(dep) + if not dep_path: + continue + dep_bin = self[dep_path][self.COL_BINB].split(', ') + if item_name in dep_bin: + dep_bin.remove(item_name) + self[dep_path][self.COL_BINB] = ', '.join(dep_bin).lstrip(', ') + + item_bin = self[item_path][self.COL_BINB].split(', ') + if item_bin: + for binb in item_bin: + binb_path = self.find_path_for_item(binb) + if not binb_path: + continue + self.exclude_item(binb_path) + + """ + Empty self.contents by setting the include of each entry to None + """ + def reset(self): + it = self.get_iter_first() + while it: + self.set(it, + self.COL_INC, False, + self.COL_BINB, "") + it = self.iter_next(it) + + self.selection_change_notification() + + def get_selected_packages(self): + packagelist = [] + + it = self.get_iter_first() + while it: + if self.get_value(it, self.COL_INC): + name = self.get_value(it, self.COL_NAME) + packagelist.append(name) + it = self.iter_next(it) + + return packagelist + + def get_user_selected_packages(self): + packagelist = [] + + it = self.get_iter_first() + while it: + if self.get_value(it, self.COL_INC): + binb = self.get_value(it, self.COL_BINB) + if binb == "User Selected": + name = self.get_value(it, self.COL_NAME) + packagelist.append(name) + it = self.iter_next(it) + + return packagelist + + def get_selected_packages_toolchain(self): + packagelist = [] + + it = self.get_iter_first() + while it: + if self.get_value(it, self.COL_INC): + name = self.get_value(it, self.COL_NAME) + if name.endswith("-dev") or name.endswith("-dbg"): + packagelist.append(name) + it = self.iter_next(it) + + return list(set(packagelist + self.__toolchain_required_packages__)); + + """ + Package model may be incomplete, therefore when calling the + set_selected_packages(), some packages will not be set included. + Return the un-set packages list. + """ + def set_selected_packages(self, packagelist, user_selected=False): + left = [] + binb = 'User Selected' if user_selected else '' + for pn in packagelist: + if pn in self.pn_path.keys(): + path = self.pn_path[pn] + self.include_item(item_path=path, binb=binb) + else: + left.append(pn) + + self.selection_change_notification() + return left + + """ + Return the selected package size, unit is B. + """ + def get_packages_size(self): + packages_size = 0 + it = self.get_iter_first() + while it: + if self.get_value(it, self.COL_INC): + str_size = self.get_value(it, self.COL_SIZE) + if not str_size: + continue + + packages_size += HobPage._string_to_size(str_size) + + it = self.iter_next(it) + return packages_size + + """ + Resync the state of included items to a backup column before performing the fadeout visible effect + """ + def resync_fadeout_column(self, model_first_iter=None): + it = model_first_iter + while it: + active = self.get_value(it, self.COL_INC) + self.set(it, self.COL_FADE_INC, active) + it = self.iter_next(it) + +# +# RecipeListModel +# +class RecipeListModel(gtk.ListStore): + """ + This class defines an gtk.ListStore subclass which will convert the output + of the bb.event.TargetsTreeGenerated event into a gtk.ListStore whilst also + providing convenience functions to access gtk.TreeModel subclasses which + provide filtered views of the data. + """ + (COL_NAME, COL_DESC, COL_LIC, COL_GROUP, COL_DEPS, COL_BINB, COL_TYPE, COL_INC, COL_IMG, COL_INSTALL, COL_PN, COL_FADE_INC, COL_SUMMARY, COL_VERSION, + COL_REVISION, COL_HOMEPAGE, COL_BUGTRACKER, COL_FILE) = range(18) + + __custom_image__ = "Start with an empty image recipe" + + __gsignals__ = { + "recipe-selection-changed" : (gobject.SIGNAL_RUN_LAST, + gobject.TYPE_NONE, + ()), + } + + """ + """ + def __init__(self): + gtk.ListStore.__init__ (self, + gobject.TYPE_STRING, + gobject.TYPE_STRING, + gobject.TYPE_STRING, + gobject.TYPE_STRING, + gobject.TYPE_STRING, + gobject.TYPE_STRING, + gobject.TYPE_STRING, + gobject.TYPE_BOOLEAN, + gobject.TYPE_BOOLEAN, + gobject.TYPE_STRING, + gobject.TYPE_STRING, + gobject.TYPE_BOOLEAN, + gobject.TYPE_STRING, + gobject.TYPE_STRING, + gobject.TYPE_STRING, + gobject.TYPE_STRING, + gobject.TYPE_STRING, + gobject.TYPE_STRING) + self.sort_column_id, self.sort_order = RecipeListModel.COL_NAME, gtk.SORT_ASCENDING + + """ + Find the model path for the item_name + Returns the path in the model or None + """ + def find_path_for_item(self, item_name): + if self.non_target_name(item_name) or item_name not in self.pn_path.keys(): + return None + else: + return self.pn_path[item_name] + + def find_item_for_path(self, item_path): + return self[item_path][self.COL_NAME] + + """ + Helper method to determine whether name is a target pn + """ + def non_target_name(self, name): + if name and ('-native' in name): + return True + return False + + """ + Helper function to determine whether an item is an item specified by filter + """ + def tree_model_filter(self, model, it, filter): + name = model.get_value(it, self.COL_NAME) + if self.non_target_name(name): + return False + + for key in filter.keys(): + if key == self.COL_NAME: + if filter[key] != 'Search recipes by name' and filter[key] != 'Search package groups by name': + if filter[key] not in name: + return False + else: + if model.get_value(it, key) not in filter[key]: + return False + self.filtered_nb += 1 + + return True + + def exclude_item_sort_func(self, model, iter1, iter2, user_data=None): + if user_data: + val1 = model.get_value(iter1, RecipeListModel.COL_NAME) + val2 = model.get_value(iter2, RecipeListModel.COL_NAME) + return self.cmp_vals(val1, val2, user_data) + else: + val1 = model.get_value(iter1, RecipeListModel.COL_FADE_INC) + val2 = model.get_value(iter2, RecipeListModel.COL_INC) + return ((val1 == True) and (val2 == False)) + + def include_item_sort_func(self, model, iter1, iter2, user_data=None): + if user_data: + val1 = model.get_value(iter1, RecipeListModel.COL_NAME) + val2 = model.get_value(iter2, RecipeListModel.COL_NAME) + return self.cmp_vals(val1, val2, user_data) + else: + val1 = model.get_value(iter1, RecipeListModel.COL_INC) + val2 = model.get_value(iter2, RecipeListModel.COL_INC) + return ((val1 == False) and (val2 == True)) + + def sort_func(self, model, iter1, iter2, user_data): + val1 = model.get_value(iter1, RecipeListModel.COL_NAME) + val2 = model.get_value(iter2, RecipeListModel.COL_NAME) + return self.cmp_vals(val1, val2, user_data) + + def cmp_vals(self, val1, val2, user_data): + if val1 is None or val2 is None: + return 0 + elif val1.startswith(user_data) and not val2.startswith(user_data): + return -1 + elif not val1.startswith(user_data) and val2.startswith(user_data): + return 1 + else: + return cmp(val1, val2) + + """ + Create, if required, and return a filtered gtk.TreeModelSort + containing only the items specified by filter + """ + def tree_model(self, filter, excluded_items_ahead=False, included_items_ahead=False, search_data=None, initial=False): + model = self.filter_new() + self.filtered_nb = 0 + model.set_visible_func(self.tree_model_filter, filter) + + sort = gtk.TreeModelSort(model) + sort.connect ('sort-column-changed', self.sort_column_changed_cb) + if initial: + sort.set_sort_column_id(RecipeListModel.COL_NAME, gtk.SORT_ASCENDING) + sort.set_default_sort_func(None) + elif excluded_items_ahead: + sort.set_default_sort_func(self.exclude_item_sort_func, search_data) + elif included_items_ahead: + sort.set_default_sort_func(self.include_item_sort_func, search_data) + else: + if search_data and search_data!='Search recipes by name' and search_data!='Search package groups by name': + sort.set_default_sort_func(self.sort_func, search_data) + else: + sort.set_sort_column_id(self.sort_column_id, self.sort_order) + sort.set_default_sort_func(None) + + sort.set_sort_func(RecipeListModel.COL_INC, self.sort_column, RecipeListModel.COL_INC) + sort.set_sort_func(RecipeListModel.COL_GROUP, self.sort_column, RecipeListModel.COL_GROUP) + sort.set_sort_func(RecipeListModel.COL_BINB, self.sort_binb_column) + sort.set_sort_func(RecipeListModel.COL_LIC, self.sort_column, RecipeListModel.COL_LIC) + return sort + + def sort_column_changed_cb (self, data): + self.sort_column_id, self.sort_order = data.get_sort_column_id () + + def sort_column(self, model, row1, row2, col): + value1 = model.get_value(row1, col) + value2 = model.get_value(row2, col) + cmp_res = cmp(value1, value2) + if cmp_res!=0: + if col==RecipeListModel.COL_INC: + return -cmp_res + else: + return cmp_res + else: + name1 = model.get_value(row1, RecipeListModel.COL_NAME) + name2 = model.get_value(row2, RecipeListModel.COL_NAME) + return cmp(name1,name2) + + def sort_binb_column(self, model, row1, row2): + value1 = model.get_value(row1, RecipeListModel.COL_BINB) + value2 = model.get_value(row2, RecipeListModel.COL_BINB) + value1_list = value1.split(', ') + value2_list = value2.split(', ') + + value1 = value1_list[0] + value2 = value2_list[0] + + cmp_res = cmp(value1, value2) + if cmp_res==0: + cmp_size = cmp(len(value1_list), len(value2_list)) + if cmp_size==0: + name1 = model.get_value(row1, RecipeListModel.COL_NAME) + name2 = model.get_value(row2, RecipeListModel.COL_NAME) + return cmp(name1,name2) + else: + return cmp_size + else: + return cmp_res + + def convert_vpath_to_path(self, view_model, view_path): + filtered_model_path = view_model.convert_path_to_child_path(view_path) + filtered_model = view_model.get_model() + + # get the path of the original model + path = filtered_model.convert_path_to_child_path(filtered_model_path) + return path + + def convert_path_to_vpath(self, view_model, path): + it = view_model.get_iter_first() + while it: + name = self.find_item_for_path(path) + view_name = view_model.get_value(it, RecipeListModel.COL_NAME) + if view_name == name: + view_path = view_model.get_path(it) + return view_path + it = view_model.iter_next(it) + return None + + """ + The populate() function takes as input the data from a + bb.event.TargetsTreeGenerated event and populates the RecipeList. + """ + def populate(self, event_model): + # First clear the model, in case repopulating + self.clear() + + # dummy image for prompt + self.set_in_list(self.__custom_image__, "Use 'Edit image recipe' to customize recipes and packages " \ + "to be included in your image ") + + for item in event_model["pn"]: + name = item + desc = event_model["pn"][item]["description"] + lic = event_model["pn"][item]["license"] + group = event_model["pn"][item]["section"] + inherits = event_model["pn"][item]["inherits"] + summary = event_model["pn"][item]["summary"] + version = event_model["pn"][item]["version"] + revision = event_model["pn"][item]["prevision"] + homepage = event_model["pn"][item]["homepage"] + bugtracker = event_model["pn"][item]["bugtracker"] + filename = event_model["pn"][item]["filename"] + install = [] + + depends = event_model["depends"].get(item, []) + event_model["rdepends-pn"].get(item, []) + + if ('packagegroup.bbclass' in " ".join(inherits)): + atype = 'packagegroup' + elif ('/image.bbclass' in " ".join(inherits)): + if "edited" not in name: + atype = 'image' + install = event_model["rdepends-pkg"].get(item, []) + event_model["rrecs-pkg"].get(item, []) + elif ('meta-' in name): + atype = 'toolchain' + elif (name == 'dummy-image' or name == 'dummy-toolchain'): + atype = 'dummy' + else: + atype = 'recipe' + + self.set(self.append(), self.COL_NAME, item, self.COL_DESC, desc, + self.COL_LIC, lic, self.COL_GROUP, group, + self.COL_DEPS, " ".join(depends), self.COL_BINB, "", + self.COL_TYPE, atype, self.COL_INC, False, + self.COL_IMG, False, self.COL_INSTALL, " ".join(install), self.COL_PN, item, + self.COL_SUMMARY, summary, self.COL_VERSION, version, self.COL_REVISION, revision, + self.COL_HOMEPAGE, homepage, self.COL_BUGTRACKER, bugtracker, + self.COL_FILE, filename) + + self.pn_path = {} + it = self.get_iter_first() + while it: + pn = self.get_value(it, self.COL_NAME) + path = self.get_path(it) + self.pn_path[pn] = path + it = self.iter_next(it) + + def set_in_list(self, item, desc): + self.set(self.append(), self.COL_NAME, item, + self.COL_DESC, desc, + self.COL_LIC, "", self.COL_GROUP, "", + self.COL_DEPS, "", self.COL_BINB, "", + self.COL_TYPE, "image", self.COL_INC, False, + self.COL_IMG, False, self.COL_INSTALL, "", self.COL_PN, item, + self.COL_SUMMARY, "", self.COL_VERSION, "", self.COL_REVISION, "", + self.COL_HOMEPAGE, "", self.COL_BUGTRACKER, "") + self.pn_path = {} + it = self.get_iter_first() + while it: + pn = self.get_value(it, self.COL_NAME) + path = self.get_path(it) + self.pn_path[pn] = path + it = self.iter_next(it) + + """ + Update the model, send out the notification. + """ + def selection_change_notification(self): + self.emit("recipe-selection-changed") + + def path_included(self, item_path): + return self[item_path][self.COL_INC] + + """ + Add this item, and any of its dependencies, to the image contents + """ + def include_item(self, item_path, binb="", image_contents=False): + if self.path_included(item_path): + return + + item_name = self[item_path][self.COL_NAME] + item_deps = self[item_path][self.COL_DEPS] + + self[item_path][self.COL_INC] = True + + item_bin = self[item_path][self.COL_BINB].split(', ') + if binb and not binb in item_bin: + item_bin.append(binb) + self[item_path][self.COL_BINB] = ', '.join(item_bin).lstrip(', ') + + # We want to do some magic with things which are brought in by the + # base image so tag them as so + if image_contents: + self[item_path][self.COL_IMG] = True + + if item_deps: + # Ensure all of the items deps are included and, where appropriate, + # add this item to their COL_BINB + for dep in item_deps.split(" "): + # If the contents model doesn't already contain dep, add it + dep_path = self.find_path_for_item(dep) + if not dep_path: + continue + dep_included = self.path_included(dep_path) + + if dep_included and not dep in item_bin: + # don't set the COL_BINB to this item if the target is an + # item in our own COL_BINB + dep_bin = self[dep_path][self.COL_BINB].split(', ') + if not item_name in dep_bin: + dep_bin.append(item_name) + self[dep_path][self.COL_BINB] = ', '.join(dep_bin).lstrip(', ') + elif not dep_included: + self.include_item(dep_path, binb=item_name, image_contents=image_contents) + dep_bin = self[item_path][self.COL_BINB].split(', ') + if self[item_path][self.COL_NAME] in dep_bin: + dep_bin.remove(self[item_path][self.COL_NAME]) + self[item_path][self.COL_BINB] = ', '.join(dep_bin).lstrip(', ') + + def exclude_item(self, item_path): + if not self.path_included(item_path): + return + + self[item_path][self.COL_INC] = False + + item_name = self[item_path][self.COL_NAME] + item_deps = self[item_path][self.COL_DEPS] + if item_deps: + for dep in item_deps.split(" "): + dep_path = self.find_path_for_item(dep) + if not dep_path: + continue + dep_bin = self[dep_path][self.COL_BINB].split(', ') + if item_name in dep_bin: + dep_bin.remove(item_name) + self[dep_path][self.COL_BINB] = ', '.join(dep_bin).lstrip(', ') + + item_bin = self[item_path][self.COL_BINB].split(', ') + if item_bin: + for binb in item_bin: + binb_path = self.find_path_for_item(binb) + if not binb_path: + continue + self.exclude_item(binb_path) + + def reset(self): + it = self.get_iter_first() + while it: + self.set(it, + self.COL_INC, False, + self.COL_BINB, "", + self.COL_IMG, False) + it = self.iter_next(it) + + self.selection_change_notification() + + """ + Returns two lists. One of user selected recipes and the other containing + all selected recipes + """ + def get_selected_recipes(self): + allrecipes = [] + userrecipes = [] + + it = self.get_iter_first() + while it: + if self.get_value(it, self.COL_INC): + name = self.get_value(it, self.COL_PN) + type = self.get_value(it, self.COL_TYPE) + if type != "image": + allrecipes.append(name) + sel = "User Selected" in self.get_value(it, self.COL_BINB) + if sel: + userrecipes.append(name) + it = self.iter_next(it) + + return list(set(userrecipes)), list(set(allrecipes)) + + def set_selected_recipes(self, recipelist): + for pn in recipelist: + if pn in self.pn_path.keys(): + path = self.pn_path[pn] + self.include_item(item_path=path, + binb="User Selected") + self.selection_change_notification() + + def get_selected_image(self): + it = self.get_iter_first() + while it: + if self.get_value(it, self.COL_INC): + name = self.get_value(it, self.COL_PN) + type = self.get_value(it, self.COL_TYPE) + if type == "image": + sel = "User Selected" in self.get_value(it, self.COL_BINB) + if sel: + return name + it = self.iter_next(it) + return None + + def set_selected_image(self, img): + if not img: + return + self.reset() + path = self.find_path_for_item(img) + self.include_item(item_path=path, + binb="User Selected", + image_contents=True) + self.selection_change_notification() + + def set_custom_image_version(self, version): + self.custom_image_version = version + + def get_custom_image_version(self): + return self.custom_image_version + + def is_custom_image(self): + return self.get_selected_image() == self.__custom_image__ diff --git a/bitbake/lib/bb/ui/crumbs/hobpages.py b/bitbake/lib/bb/ui/crumbs/hobpages.py new file mode 100755 index 0000000000..0fd3598c3a --- /dev/null +++ b/bitbake/lib/bb/ui/crumbs/hobpages.py @@ -0,0 +1,128 @@ +#!/usr/bin/env python +# +# BitBake Graphical GTK User Interface +# +# Copyright (C) 2012 Intel Corporation +# +# Authored by Dongxiao Xu +# Authored by Shane Wang +# +# 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 +from bb.ui.crumbs.hobcolor import HobColors +from bb.ui.crumbs.hobwidget import hwc + +# +# HobPage: the super class for all Hob-related pages +# +class HobPage (gtk.VBox): + + def __init__(self, builder, title = None): + super(HobPage, self).__init__(False, 0) + self.builder = builder + self.builder_width, self.builder_height = self.builder.size_request() + + if not title: + self.title = "Hob -- Image Creator" + else: + self.title = title + self.title_label = gtk.Label() + + self.box_group_area = gtk.VBox(False, 12) + self.box_group_area.set_size_request(self.builder_width - 73 - 73, self.builder_height - 88 - 15 - 15) + self.group_align = gtk.Alignment(xalign = 0, yalign=0.5, xscale=1, yscale=1) + self.group_align.set_padding(15, 15, 73, 73) + self.group_align.add(self.box_group_area) + self.box_group_area.set_homogeneous(False) + + def set_title(self, title): + self.title = title + self.title_label.set_markup("%s" % self.title) + + def add_onto_top_bar(self, widget = None, padding = 0): + # the top button occupies 1/7 of the page height + # setup an event box + eventbox = gtk.EventBox() + style = eventbox.get_style().copy() + style.bg[gtk.STATE_NORMAL] = eventbox.get_colormap().alloc_color(HobColors.LIGHT_GRAY, False, False) + eventbox.set_style(style) + eventbox.set_size_request(-1, 88) + + hbox = gtk.HBox() + + self.title_label = gtk.Label() + self.title_label.set_markup("%s" % self.title) + hbox.pack_start(self.title_label, expand=False, fill=False, padding=20) + + if widget: + # add the widget in the event box + hbox.pack_end(widget, expand=False, fill=False, padding=padding) + eventbox.add(hbox) + + return eventbox + + def span_tag(self, size="medium", weight="normal", forground="#1c1c1c"): + span_tag = "weight='%s' foreground='%s' size='%s'" % (weight, forground, size) + return span_tag + + def append_toolbar_button(self, toolbar, buttonname, icon_disp, icon_hovor, tip, cb): + # Create a button and append it on the toolbar according to button name + icon = gtk.Image() + icon_display = icon_disp + icon_hover = icon_hovor + pix_buffer = gtk.gdk.pixbuf_new_from_file(icon_display) + icon.set_from_pixbuf(pix_buffer) + tip_text = tip + button = toolbar.append_item(buttonname, tip, None, icon, cb) + return button + + @staticmethod + def _size_to_string(size): + try: + if not size: + size_str = "0 B" + else: + if len(str(int(size))) > 6: + size_str = '%.1f' % (size*1.0/(1024*1024)) + ' MB' + elif len(str(int(size))) > 3: + size_str = '%.1f' % (size*1.0/1024) + ' KB' + else: + size_str = str(size) + ' B' + except: + size_str = "0 B" + return size_str + + @staticmethod + def _string_to_size(str_size): + try: + if not str_size: + size = 0 + else: + unit = str_size.split() + if len(unit) > 1: + if unit[1] == 'MB': + size = float(unit[0])*1024*1024 + elif unit[1] == 'KB': + size = float(unit[0])*1024 + elif unit[1] == 'B': + size = float(unit[0]) + else: + size = 0 + else: + size = float(unit[0]) + except: + size = 0 + return size + diff --git a/bitbake/lib/bb/ui/crumbs/hobwidget.py b/bitbake/lib/bb/ui/crumbs/hobwidget.py new file mode 100644 index 0000000000..2b969c146e --- /dev/null +++ b/bitbake/lib/bb/ui/crumbs/hobwidget.py @@ -0,0 +1,904 @@ +# BitBake Graphical GTK User Interface +# +# Copyright (C) 2011-2012 Intel Corporation +# +# Authored by Dongxiao Xu +# Authored by Shane Wang +# +# 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 os +import os.path +import sys +import pango, pangocairo +import cairo +import math + +from bb.ui.crumbs.hobcolor import HobColors +from bb.ui.crumbs.persistenttooltip import PersistentTooltip + +class hwc: + + MAIN_WIN_WIDTH = 1024 + MAIN_WIN_HEIGHT = 700 + +class hic: + + HOB_ICON_BASE_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), ("ui/icons/")) + + ICON_RCIPE_DISPLAY_FILE = os.path.join(HOB_ICON_BASE_DIR, ('recipe/recipe_display.png')) + ICON_RCIPE_HOVER_FILE = os.path.join(HOB_ICON_BASE_DIR, ('recipe/recipe_hover.png')) + ICON_PACKAGES_DISPLAY_FILE = os.path.join(HOB_ICON_BASE_DIR, ('packages/packages_display.png')) + ICON_PACKAGES_HOVER_FILE = os.path.join(HOB_ICON_BASE_DIR, ('packages/packages_hover.png')) + ICON_LAYERS_DISPLAY_FILE = os.path.join(HOB_ICON_BASE_DIR, ('layers/layers_display.png')) + ICON_LAYERS_HOVER_FILE = os.path.join(HOB_ICON_BASE_DIR, ('layers/layers_hover.png')) + ICON_IMAGES_DISPLAY_FILE = os.path.join(HOB_ICON_BASE_DIR, ('images/images_display.png')) + ICON_IMAGES_HOVER_FILE = os.path.join(HOB_ICON_BASE_DIR, ('images/images_hover.png')) + ICON_SETTINGS_DISPLAY_FILE = os.path.join(HOB_ICON_BASE_DIR, ('settings/settings_display.png')) + ICON_SETTINGS_HOVER_FILE = os.path.join(HOB_ICON_BASE_DIR, ('settings/settings_hover.png')) + ICON_INFO_DISPLAY_FILE = os.path.join(HOB_ICON_BASE_DIR, ('info/info_display.png')) + ICON_INFO_HOVER_FILE = os.path.join(HOB_ICON_BASE_DIR, ('info/info_hover.png')) + ICON_INDI_CONFIRM_FILE = os.path.join(HOB_ICON_BASE_DIR, ('indicators/confirmation.png')) + ICON_INDI_ERROR_FILE = os.path.join(HOB_ICON_BASE_DIR, ('indicators/denied.png')) + ICON_INDI_REMOVE_FILE = os.path.join(HOB_ICON_BASE_DIR, ('indicators/remove.png')) + ICON_INDI_REMOVE_HOVER_FILE = os.path.join(HOB_ICON_BASE_DIR, ('indicators/remove-hover.png')) + ICON_INDI_ADD_FILE = os.path.join(HOB_ICON_BASE_DIR, ('indicators/add.png')) + ICON_INDI_ADD_HOVER_FILE = os.path.join(HOB_ICON_BASE_DIR, ('indicators/add-hover.png')) + ICON_INDI_REFRESH_FILE = os.path.join(HOB_ICON_BASE_DIR, ('indicators/refresh.png')) + ICON_INDI_ALERT_FILE = os.path.join(HOB_ICON_BASE_DIR, ('indicators/alert.png')) + ICON_INDI_TICK_FILE = os.path.join(HOB_ICON_BASE_DIR, ('indicators/tick.png')) + ICON_INDI_INFO_FILE = os.path.join(HOB_ICON_BASE_DIR, ('indicators/info.png')) + +class HobViewTable (gtk.VBox): + """ + A VBox to contain the table for different recipe views and package view + """ + __gsignals__ = { + "toggled" : (gobject.SIGNAL_RUN_LAST, + gobject.TYPE_NONE, + (gobject.TYPE_PYOBJECT, + gobject.TYPE_STRING, + gobject.TYPE_INT, + gobject.TYPE_PYOBJECT,)), + "row-activated" : (gobject.SIGNAL_RUN_LAST, + gobject.TYPE_NONE, + (gobject.TYPE_PYOBJECT, + gobject.TYPE_PYOBJECT,)), + "cell-fadeinout-stopped" : (gobject.SIGNAL_RUN_LAST, + gobject.TYPE_NONE, + (gobject.TYPE_PYOBJECT, + gobject.TYPE_PYOBJECT, + gobject.TYPE_PYOBJECT,)), + } + + def __init__(self, columns, name): + gtk.VBox.__init__(self, False, 6) + self.table_tree = gtk.TreeView() + self.table_tree.set_headers_visible(True) + self.table_tree.set_headers_clickable(True) + self.table_tree.set_rules_hint(True) + self.table_tree.set_enable_tree_lines(True) + self.table_tree.get_selection().set_mode(gtk.SELECTION_SINGLE) + self.toggle_columns = [] + self.table_tree.connect("row-activated", self.row_activated_cb) + self.top_bar = None + self.tab_name = name + + for i, column in enumerate(columns): + col_name = column['col_name'] + col = gtk.TreeViewColumn(col_name) + col.set_clickable(True) + col.set_resizable(True) + if self.tab_name.startswith('Included'): + if col_name!='Included': + col.set_sort_column_id(column['col_id']) + else: + col.set_sort_column_id(column['col_id']) + if 'col_min' in column.keys(): + col.set_min_width(column['col_min']) + if 'col_max' in column.keys(): + col.set_max_width(column['col_max']) + if 'expand' in column.keys(): + col.set_expand(True) + self.table_tree.append_column(col) + + if (not 'col_style' in column.keys()) or column['col_style'] == 'text': + cell = gtk.CellRendererText() + col.pack_start(cell, True) + col.set_attributes(cell, text=column['col_id']) + if 'col_t_id' in column.keys(): + col.add_attribute(cell, 'font', column['col_t_id']) + elif column['col_style'] == 'check toggle': + cell = HobCellRendererToggle() + cell.set_property('activatable', True) + cell.connect("toggled", self.toggled_cb, i, self.table_tree) + cell.connect_render_state_changed(self.stop_cell_fadeinout_cb, self.table_tree) + self.toggle_id = i + col.pack_end(cell, True) + col.set_attributes(cell, active=column['col_id']) + self.toggle_columns.append(col_name) + if 'col_group' in column.keys(): + col.set_cell_data_func(cell, self.set_group_number_cb) + elif column['col_style'] == 'radio toggle': + cell = gtk.CellRendererToggle() + cell.set_property('activatable', True) + cell.set_radio(True) + cell.connect("toggled", self.toggled_cb, i, self.table_tree) + self.toggle_id = i + col.pack_end(cell, True) + col.set_attributes(cell, active=column['col_id']) + self.toggle_columns.append(col_name) + elif column['col_style'] == 'binb': + cell = gtk.CellRendererText() + col.pack_start(cell, True) + col.set_cell_data_func(cell, self.display_binb_cb, column['col_id']) + if 'col_t_id' in column.keys(): + col.add_attribute(cell, 'font', column['col_t_id']) + + self.scroll = gtk.ScrolledWindow() + self.scroll.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC) + self.scroll.add(self.table_tree) + + self.pack_end(self.scroll, True, True, 0) + + def add_no_result_bar(self, entry): + color = HobColors.KHAKI + self.top_bar = gtk.EventBox() + self.top_bar.set_size_request(-1, 70) + self.top_bar.modify_bg(gtk.STATE_NORMAL, gtk.gdk.color_parse(color)) + self.top_bar.set_flags(gtk.CAN_DEFAULT) + self.top_bar.grab_default() + + no_result_tab = gtk.Table(5, 20, True) + self.top_bar.add(no_result_tab) + + label = gtk.Label() + label.set_alignment(0.0, 0.5) + title = "No results matching your search" + label.set_markup("%s" % title) + no_result_tab.attach(label, 1, 14, 1, 4) + + clear_button = HobButton("Clear search") + clear_button.set_tooltip_text("Clear search query") + clear_button.connect('clicked', self.set_search_entry_clear_cb, entry) + no_result_tab.attach(clear_button, 16, 19, 1, 4) + + self.pack_start(self.top_bar, False, True, 12) + self.top_bar.show_all() + + def set_search_entry_clear_cb(self, button, search): + if search.get_editable() == True: + search.set_text("") + search.set_icon_sensitive(gtk.ENTRY_ICON_SECONDARY, False) + search.grab_focus() + + def display_binb_cb(self, col, cell, model, it, col_id): + binb = model.get_value(it, col_id) + # Just display the first item + if binb: + bin = binb.split(', ') + total_no = len(bin) + if total_no > 1 and bin[0] == "User Selected": + if total_no > 2: + present_binb = bin[1] + ' (+' + str(total_no - 1) + ')' + else: + present_binb = bin[1] + else: + if total_no > 1: + present_binb = bin[0] + ' (+' + str(total_no - 1) + ')' + else: + present_binb = bin[0] + cell.set_property('text', present_binb) + else: + cell.set_property('text', "") + return True + + def set_model(self, tree_model): + self.table_tree.set_model(tree_model) + + def toggle_default(self): + model = self.table_tree.get_model() + if not model: + return + iter = model.get_iter_first() + if iter: + rowpath = model.get_path(iter) + model[rowpath][self.toggle_id] = True + + def toggled_cb(self, cell, path, columnid, tree): + self.emit("toggled", cell, path, columnid, tree) + + def row_activated_cb(self, tree, path, view_column): + if not view_column.get_title() in self.toggle_columns: + self.emit("row-activated", tree.get_model(), path) + + def stop_cell_fadeinout_cb(self, ctrl, cell, tree): + self.emit("cell-fadeinout-stopped", ctrl, cell, tree) + + def set_group_number_cb(self, col, cell, model, iter): + if model and (model.iter_parent(iter) == None): + cell.cell_attr["number_of_children"] = model.iter_n_children(iter) + else: + cell.cell_attr["number_of_children"] = 0 + + def connect_group_selection(self, cb_func): + self.table_tree.get_selection().connect("changed", cb_func) + +""" +A method to calculate a softened value for the colour of widget when in the +provided state. + +widget: the widget whose style to use +state: the state of the widget to use the style for + +Returns a string value representing the softened colour +""" +def soften_color(widget, state=gtk.STATE_NORMAL): + # this colour munging routine is heavily inspired bu gdu_util_get_mix_color() + # from gnome-disk-utility: + # http://git.gnome.org/browse/gnome-disk-utility/tree/src/gdu-gtk/gdu-gtk.c?h=gnome-3-0 + blend = 0.7 + style = widget.get_style() + color = style.text[state] + color.red = color.red * blend + style.base[state].red * (1.0 - blend) + color.green = color.green * blend + style.base[state].green * (1.0 - blend) + color.blue = color.blue * blend + style.base[state].blue * (1.0 - blend) + return color.to_string() + +class BaseHobButton(gtk.Button): + """ + A gtk.Button subclass which follows the visual design of Hob for primary + action buttons + + label: the text to display as the button's label + """ + def __init__(self, label): + gtk.Button.__init__(self, label) + HobButton.style_button(self) + + @staticmethod + def style_button(button): + style = button.get_style() + style = gtk.rc_get_style_by_paths(gtk.settings_get_default(), 'gtk-button', 'gtk-button', gobject.TYPE_NONE) + + button.set_flags(gtk.CAN_DEFAULT) + button.grab_default() + +# label = "%s" % gobject.markup_escape_text(button.get_label()) + label = button.get_label() + button.set_label(label) + button.child.set_use_markup(True) + +class HobButton(BaseHobButton): + """ + A gtk.Button subclass which follows the visual design of Hob for primary + action buttons + + label: the text to display as the button's label + """ + def __init__(self, label): + BaseHobButton.__init__(self, label) + HobButton.style_button(self) + +class HobAltButton(BaseHobButton): + """ + A gtk.Button subclass which has no relief, and so is more discrete + """ + def __init__(self, label): + BaseHobButton.__init__(self, label) + HobAltButton.style_button(self) + + """ + A callback for the state-changed event to ensure the text is displayed + differently when the widget is not sensitive + """ + @staticmethod + def desensitise_on_state_change_cb(button, state): + if not button.get_property("sensitive"): + HobAltButton.set_text(button, False) + else: + HobAltButton.set_text(button, True) + + """ + Set the button label with an appropriate colour for the current widget state + """ + @staticmethod + def set_text(button, sensitive=True): + if sensitive: + colour = HobColors.PALE_BLUE + else: + colour = HobColors.LIGHT_GRAY + button.set_label("%s" % (colour, gobject.markup_escape_text(button.text))) + button.child.set_use_markup(True) + +class HobImageButton(gtk.Button): + """ + A gtk.Button with an icon and two rows of text, the second of which is + displayed in a blended colour. + + primary_text: the main button label + secondary_text: optional second line of text + icon_path: path to the icon file to display on the button + """ + def __init__(self, primary_text, secondary_text="", icon_path="", hover_icon_path=""): + gtk.Button.__init__(self) + self.set_relief(gtk.RELIEF_NONE) + + self.icon_path = icon_path + self.hover_icon_path = hover_icon_path + + hbox = gtk.HBox(False, 10) + hbox.show() + self.add(hbox) + self.icon = gtk.Image() + self.icon.set_from_file(self.icon_path) + self.icon.set_alignment(0.5, 0.0) + self.icon.show() + if self.hover_icon_path and len(self.hover_icon_path): + self.connect("enter-notify-event", self.set_hover_icon_cb) + self.connect("leave-notify-event", self.set_icon_cb) + hbox.pack_start(self.icon, False, False, 0) + label = gtk.Label() + label.set_alignment(0.0, 0.5) + colour = soften_color(label) + mark = "%s\n%s" % (primary_text, colour, secondary_text) + label.set_markup(mark) + label.show() + hbox.pack_start(label, True, True, 0) + + def set_hover_icon_cb(self, widget, event): + self.icon.set_from_file(self.hover_icon_path) + + def set_icon_cb(self, widget, event): + self.icon.set_from_file(self.icon_path) + +class HobInfoButton(gtk.EventBox): + """ + This class implements a button-like widget per the Hob visual and UX designs + which will display a persistent tooltip, with the contents of tip_markup, when + clicked. + + tip_markup: the Pango Markup to be displayed in the persistent tooltip + """ + def __init__(self, tip_markup, parent=None): + gtk.EventBox.__init__(self) + self.image = gtk.Image() + self.image.set_from_file( + hic.ICON_INFO_DISPLAY_FILE) + self.image.show() + self.add(self.image) + self.tip_markup = tip_markup + self.my_parent = parent + + self.set_events(gtk.gdk.BUTTON_RELEASE | + gtk.gdk.ENTER_NOTIFY_MASK | + gtk.gdk.LEAVE_NOTIFY_MASK) + + self.connect("button-release-event", self.button_release_cb) + self.connect("enter-notify-event", self.mouse_in_cb) + self.connect("leave-notify-event", self.mouse_out_cb) + + """ + When the mouse click is released emulate a button-click and show the associated + PersistentTooltip + """ + def button_release_cb(self, widget, event): + from bb.ui.crumbs.hig.propertydialog import PropertyDialog + self.dialog = PropertyDialog(title = '', + parent = self.my_parent, + information = self.tip_markup, + flags = gtk.DIALOG_DESTROY_WITH_PARENT + | gtk.DIALOG_NO_SEPARATOR) + + button = self.dialog.add_button("Close", gtk.RESPONSE_CANCEL) + HobAltButton.style_button(button) + button.connect("clicked", lambda w: self.dialog.destroy()) + self.dialog.show_all() + self.dialog.run() + + """ + Change to the prelight image when the mouse enters the widget + """ + def mouse_in_cb(self, widget, event): + self.image.set_from_file(hic.ICON_INFO_HOVER_FILE) + + """ + Change to the stock image when the mouse enters the widget + """ + def mouse_out_cb(self, widget, event): + self.image.set_from_file(hic.ICON_INFO_DISPLAY_FILE) + +class HobIndicator(gtk.DrawingArea): + def __init__(self, count): + gtk.DrawingArea.__init__(self) + # Set no window for transparent background + self.set_has_window(False) + self.set_size_request(38,38) + # We need to pass through button clicks + self.add_events(gtk.gdk.BUTTON_PRESS_MASK | gtk.gdk.BUTTON_RELEASE_MASK) + + self.connect('expose-event', self.expose) + + self.count = count + self.color = HobColors.GRAY + + def expose(self, widget, event): + if self.count and self.count > 0: + ctx = widget.window.cairo_create() + + x, y, w, h = self.allocation + + ctx.set_operator(cairo.OPERATOR_OVER) + ctx.set_source_color(gtk.gdk.color_parse(self.color)) + ctx.translate(w/2, h/2) + ctx.arc(x, y, min(w,h)/2 - 2, 0, 2*math.pi) + ctx.fill_preserve() + + layout = self.create_pango_layout(str(self.count)) + textw, texth = layout.get_pixel_size() + x = (w/2)-(textw/2) + x + y = (h/2) - (texth/2) + y + ctx.move_to(x, y) + self.window.draw_layout(self.style.light_gc[gtk.STATE_NORMAL], int(x), int(y), layout) + + def set_count(self, count): + self.count = count + + def set_active(self, active): + if active: + self.color = HobColors.DEEP_RED + else: + self.color = HobColors.GRAY + +class HobTabLabel(gtk.HBox): + def __init__(self, text, count=0): + gtk.HBox.__init__(self, False, 0) + self.indicator = HobIndicator(count) + self.indicator.show() + self.pack_end(self.indicator, False, False) + self.lbl = gtk.Label(text) + self.lbl.set_alignment(0.0, 0.5) + self.lbl.show() + self.pack_end(self.lbl, True, True, 6) + + def set_count(self, count): + self.indicator.set_count(count) + + def set_active(self, active=True): + self.indicator.set_active(active) + +class HobNotebook(gtk.Notebook): + def __init__(self): + gtk.Notebook.__init__(self) + self.set_property('homogeneous', True) + + self.pages = [] + + self.search = None + self.search_focus = False + self.page_changed = False + + self.connect("switch-page", self.page_changed_cb) + + self.show_all() + + def page_changed_cb(self, nb, page, page_num): + for p, lbl in enumerate(self.pages): + if p == page_num: + lbl.set_active() + else: + lbl.set_active(False) + + if self.search: + self.page_changed = True + self.reset_entry(self.search, page_num) + + def append_page(self, child, tab_label, tab_tooltip=None): + label = HobTabLabel(tab_label) + if tab_tooltip: + label.set_tooltip_text(tab_tooltip) + label.set_active(False) + self.pages.append(label) + gtk.Notebook.append_page(self, child, label) + + def set_entry(self, names, tips): + self.search = gtk.Entry() + self.search_names = names + self.search_tips = tips + style = self.search.get_style() + style.text[gtk.STATE_NORMAL] = self.get_colormap().alloc_color(HobColors.GRAY, False, False) + self.search.set_style(style) + self.search.set_text(names[0]) + self.search.set_tooltip_text(self.search_tips[0]) + self.search.props.has_tooltip = True + + self.search.set_editable(False) + self.search.set_icon_from_stock(gtk.ENTRY_ICON_SECONDARY, gtk.STOCK_CLEAR) + self.search.set_icon_sensitive(gtk.ENTRY_ICON_SECONDARY, False) + self.search.connect("icon-release", self.set_search_entry_clear_cb) + self.search.set_width_chars(30) + self.search.show() + + self.search.connect("focus-in-event", self.set_search_entry_editable_cb) + self.search.connect("focus-out-event", self.set_search_entry_reset_cb) + self.set_action_widget(self.search, gtk.PACK_END) + + def show_indicator_icon(self, title, number): + for child in self.pages: + if child.lbl.get_label() == title: + child.set_count(number) + + def hide_indicator_icon(self, title): + for child in self.pages: + if child.lbl.get_label() == title: + child.set_count(0) + + def set_search_entry_editable_cb(self, search, event): + self.search_focus = True + search.set_editable(True) + text = search.get_text() + if text in self.search_names: + search.set_text("") + style = self.search.get_style() + style.text[gtk.STATE_NORMAL] = self.get_colormap().alloc_color(HobColors.BLACK, False, False) + search.set_style(style) + + def set_search_entry_reset_cb(self, search, event): + page_num = self.get_current_page() + text = search.get_text() + if not text: + self.reset_entry(search, page_num) + + def reset_entry(self, entry, page_num): + style = entry.get_style() + style.text[gtk.STATE_NORMAL] = self.get_colormap().alloc_color(HobColors.GRAY, False, False) + entry.set_style(style) + entry.set_text(self.search_names[page_num]) + entry.set_tooltip_text(self.search_tips[page_num]) + entry.set_editable(False) + entry.set_icon_sensitive(gtk.ENTRY_ICON_SECONDARY, False) + + def set_search_entry_clear_cb(self, search, icon_pos, event): + if search.get_editable() == True: + search.set_text("") + search.set_icon_sensitive(gtk.ENTRY_ICON_SECONDARY, False) + search.grab_focus() + + def set_page(self, title): + for child in self.pages: + if child.lbl.get_label() == title: + child.grab_focus() + self.set_current_page(self.pages.index(child)) + return + +class HobWarpCellRendererText(gtk.CellRendererText): + def __init__(self, col_number): + gtk.CellRendererText.__init__(self) + self.set_property("wrap-mode", pango.WRAP_WORD_CHAR) + self.set_property("wrap-width", 300) # default value wrap width is 300 + self.col_n = col_number + + def do_render(self, window, widget, background_area, cell_area, expose_area, flags): + if widget: + self.props.wrap_width = self.get_resized_wrap_width(widget, widget.get_column(self.col_n)) + return gtk.CellRendererText.do_render(self, window, widget, background_area, cell_area, expose_area, flags) + + def get_resized_wrap_width(self, treeview, column): + otherCols = [] + for col in treeview.get_columns(): + if col != column: + otherCols.append(col) + adjwidth = treeview.allocation.width - sum(c.get_width() for c in otherCols) + adjwidth -= treeview.style_get_property("horizontal-separator") * 4 + if self.props.wrap_width == adjwidth or adjwidth <= 0: + adjwidth = self.props.wrap_width + return adjwidth + +gobject.type_register(HobWarpCellRendererText) + +class HobIconChecker(hic): + def set_hob_icon_to_stock_icon(self, file_path, stock_id=""): + try: + pixbuf = gtk.gdk.pixbuf_new_from_file(file_path) + except Exception, e: + return None + + if stock_id and (gtk.icon_factory_lookup_default(stock_id) == None): + icon_factory = gtk.IconFactory() + icon_factory.add_default() + icon_factory.add(stock_id, gtk.IconSet(pixbuf)) + gtk.stock_add([(stock_id, '_label', 0, 0, '')]) + + return icon_factory.lookup(stock_id) + + return None + + """ + For make hob icon consistently by request, and avoid icon view diff by system or gtk version, we use some 'hob icon' to replace the 'gtk icon'. + this function check the stock_id and make hob_id to replaced the gtk_id then return it or "" + """ + def check_stock_icon(self, stock_name=""): + HOB_CHECK_STOCK_NAME = { + ('hic-dialog-info', 'gtk-dialog-info', 'dialog-info') : self.ICON_INDI_INFO_FILE, + ('hic-ok', 'gtk-ok', 'ok') : self.ICON_INDI_TICK_FILE, + ('hic-dialog-error', 'gtk-dialog-error', 'dialog-error') : self.ICON_INDI_ERROR_FILE, + ('hic-dialog-warning', 'gtk-dialog-warning', 'dialog-warning') : self.ICON_INDI_ALERT_FILE, + ('hic-task-refresh', 'gtk-execute', 'execute') : self.ICON_INDI_REFRESH_FILE, + } + valid_stock_id = stock_name + if stock_name: + for names, path in HOB_CHECK_STOCK_NAME.iteritems(): + if stock_name in names: + valid_stock_id = names[0] + if not gtk.icon_factory_lookup_default(valid_stock_id): + self.set_hob_icon_to_stock_icon(path, valid_stock_id) + + return valid_stock_id + +class HobCellRendererController(gobject.GObject): + (MODE_CYCLE_RUNNING, MODE_ONE_SHORT) = range(2) + __gsignals__ = { + "run-timer-stopped" : (gobject.SIGNAL_RUN_LAST, + gobject.TYPE_NONE, + ()), + } + def __init__(self, runningmode=MODE_CYCLE_RUNNING, is_draw_row=False): + gobject.GObject.__init__(self) + self.timeout_id = None + self.current_angle_pos = 0.0 + self.step_angle = 0.0 + self.tree_headers_height = 0 + self.running_cell_areas = [] + self.running_mode = runningmode + self.is_queue_draw_row_area = is_draw_row + self.force_stop_enable = False + + def is_active(self): + if self.timeout_id: + return True + else: + return False + + def reset_run(self): + self.force_stop() + self.running_cell_areas = [] + self.current_angle_pos = 0.0 + self.step_angle = 0.0 + + ''' time_iterval: (1~1000)ms, which will be as the basic interval count for timer + init_usrdata: the current data which related the progress-bar will be at + min_usrdata: the range of min of user data + max_usrdata: the range of max of user data + step: each step which you want to progress + Note: the init_usrdata should in the range of from min to max, and max should > min + step should < (max - min) + ''' + def start_run(self, time_iterval, init_usrdata, min_usrdata, max_usrdata, step, tree): + if (not time_iterval) or (not max_usrdata): + return + usr_range = (max_usrdata - min_usrdata) * 1.0 + self.current_angle_pos = (init_usrdata * 1.0) / usr_range + self.step_angle = (step * 1) / usr_range + self.timeout_id = gobject.timeout_add(int(time_iterval), + self.make_image_on_progressing_cb, tree) + self.tree_headers_height = self.get_treeview_headers_height(tree) + self.force_stop_enable = False + + def force_stop(self): + self.emit("run-timer-stopped") + self.force_stop_enable = True + if self.timeout_id: + if gobject.source_remove(self.timeout_id): + self.timeout_id = None + + def on_draw_pixbuf_cb(self, pixbuf, cr, x, y, img_width, img_height, do_refresh=True): + if pixbuf: + r = max(img_width/2, img_height/2) + cr.translate(x + r, y + r) + if do_refresh: + cr.rotate(2 * math.pi * self.current_angle_pos) + + cr.set_source_pixbuf(pixbuf, -img_width/2, -img_height/2) + cr.paint() + + def on_draw_fadeinout_cb(self, cr, color, x, y, width, height, do_fadeout=True): + if do_fadeout: + alpha = self.current_angle_pos * 0.8 + else: + alpha = (1.0 - self.current_angle_pos) * 0.8 + + cr.set_source_rgba(color.red, color.green, color.blue, alpha) + cr.rectangle(x, y, width, height) + cr.fill() + + def get_treeview_headers_height(self, tree): + if tree and (tree.get_property("headers-visible") == True): + height = tree.get_allocation().height - tree.get_bin_window().get_size()[1] + return height + + return 0 + + def make_image_on_progressing_cb(self, tree): + self.current_angle_pos += self.step_angle + if self.running_mode == self.MODE_CYCLE_RUNNING: + if (self.current_angle_pos >= 1): + self.current_angle_pos = 0 + else: + if self.current_angle_pos > 1: + self.force_stop() + return False + + if self.is_queue_draw_row_area: + for path in self.running_cell_areas: + rect = tree.get_cell_area(path, tree.get_column(0)) + row_x, _, row_width, _ = tree.get_visible_rect() + tree.queue_draw_area(row_x, rect.y + self.tree_headers_height, row_width, rect.height) + else: + for rect in self.running_cell_areas: + tree.queue_draw_area(rect.x, rect.y + self.tree_headers_height, rect.width, rect.height) + + return (not self.force_stop_enable) + + def append_running_cell_area(self, cell_area): + if cell_area and (cell_area not in self.running_cell_areas): + self.running_cell_areas.append(cell_area) + + def remove_running_cell_area(self, cell_area): + if cell_area in self.running_cell_areas: + self.running_cell_areas.remove(cell_area) + if not self.running_cell_areas: + self.reset_run() + +gobject.type_register(HobCellRendererController) + +class HobCellRendererPixbuf(gtk.CellRendererPixbuf): + def __init__(self): + gtk.CellRendererPixbuf.__init__(self) + self.control = HobCellRendererController() + # add icon checker for make the gtk-icon transfer to hob-icon + self.checker = HobIconChecker() + self.set_property("stock-size", gtk.ICON_SIZE_DND) + + def get_pixbuf_from_stock_icon(self, widget, stock_id="", size=gtk.ICON_SIZE_DIALOG): + if widget and stock_id and gtk.icon_factory_lookup_default(stock_id): + return widget.render_icon(stock_id, size) + + return None + + def set_icon_name_to_id(self, new_name): + if new_name and type(new_name) == str: + # check the name is need to transfer to hob icon or not + name = self.checker.check_stock_icon(new_name) + if name.startswith("hic") or name.startswith("gtk"): + stock_id = name + else: + stock_id = 'gtk-' + name + + return stock_id + + ''' render cell exactly, "icon-name" is priority + if use the 'hic-task-refresh' will make the pix animation + if 'pix' will change the pixbuf for it from the pixbuf or image. + ''' + def do_render(self, window, tree, background_area,cell_area, expose_area, flags): + if (not self.control) or (not tree): + return + + x, y, w, h = self.on_get_size(tree, cell_area) + x += cell_area.x + y += cell_area.y + w -= 2 * self.get_property("xpad") + h -= 2 * self.get_property("ypad") + + stock_id = "" + if self.props.icon_name: + stock_id = self.set_icon_name_to_id(self.props.icon_name) + elif self.props.stock_id: + stock_id = self.props.stock_id + elif self.props.pixbuf: + pix = self.props.pixbuf + else: + return + + if stock_id: + pix = self.get_pixbuf_from_stock_icon(tree, stock_id, self.props.stock_size) + if stock_id == 'hic-task-refresh': + self.control.append_running_cell_area(cell_area) + if self.control.is_active(): + self.control.on_draw_pixbuf_cb(pix, window.cairo_create(), x, y, w, h, True) + else: + self.control.start_run(200, 0, 0, 1000, 150, tree) + else: + self.control.remove_running_cell_area(cell_area) + self.control.on_draw_pixbuf_cb(pix, window.cairo_create(), x, y, w, h, False) + + def on_get_size(self, widget, cell_area): + if self.props.icon_name or self.props.pixbuf or self.props.stock_id: + w, h = gtk.icon_size_lookup(self.props.stock_size) + calc_width = self.get_property("xpad") * 2 + w + calc_height = self.get_property("ypad") * 2 + h + x_offset = 0 + y_offset = 0 + if cell_area and w > 0 and h > 0: + x_offset = self.get_property("xalign") * (cell_area.width - calc_width - self.get_property("xpad")) + y_offset = self.get_property("yalign") * (cell_area.height - calc_height - self.get_property("ypad")) + + return x_offset, y_offset, w, h + + return 0, 0, 0, 0 + +gobject.type_register(HobCellRendererPixbuf) + +class HobCellRendererToggle(gtk.CellRendererToggle): + def __init__(self): + gtk.CellRendererToggle.__init__(self) + self.ctrl = HobCellRendererController(is_draw_row=True) + self.ctrl.running_mode = self.ctrl.MODE_ONE_SHORT + self.cell_attr = {"fadeout": False, "number_of_children": 0} + + def do_render(self, window, widget, background_area, cell_area, expose_area, flags): + if (not self.ctrl) or (not widget): + return + + if flags & gtk.CELL_RENDERER_SELECTED: + state = gtk.STATE_SELECTED + else: + state = gtk.STATE_NORMAL + + if self.ctrl.is_active(): + path = widget.get_path_at_pos(cell_area.x + cell_area.width/2, cell_area.y + cell_area.height/2) + # sometimes the parameters of cell_area will be a negative number,such as pull up down the scroll bar + # it's over the tree container range, so the path will be bad + if not path: return + path = path[0] + if path in self.ctrl.running_cell_areas: + cr = window.cairo_create() + color = widget.get_style().base[state] + + row_x, _, row_width, _ = widget.get_visible_rect() + border_y = self.get_property("ypad") + self.ctrl.on_draw_fadeinout_cb(cr, color, row_x, cell_area.y - border_y, row_width, \ + cell_area.height + border_y * 2, self.cell_attr["fadeout"]) + # draw number of a group + if self.cell_attr["number_of_children"]: + text = "%d pkg" % self.cell_attr["number_of_children"] + pangolayout = widget.create_pango_layout(text) + textw, texth = pangolayout.get_pixel_size() + x = cell_area.x + (cell_area.width/2) - (textw/2) + y = cell_area.y + (cell_area.height/2) - (texth/2) + + widget.style.paint_layout(window, state, True, cell_area, widget, "checkbox", x, y, pangolayout) + else: + return gtk.CellRendererToggle.do_render(self, window, widget, background_area, cell_area, expose_area, flags) + + '''delay: normally delay time is 1000ms + cell_list: whilch cells need to be render + ''' + def fadeout(self, tree, delay, cell_list=None): + if (delay < 200) or (not tree): + return + self.cell_attr["fadeout"] = True + self.ctrl.running_cell_areas = cell_list + self.ctrl.start_run(200, 0, 0, delay, (delay * 200 / 1000), tree) + + def connect_render_state_changed(self, func, usrdata=None): + if not func: + return + if usrdata: + self.ctrl.connect("run-timer-stopped", func, self, usrdata) + else: + self.ctrl.connect("run-timer-stopped", func, self) + +gobject.type_register(HobCellRendererToggle) diff --git a/bitbake/lib/bb/ui/crumbs/imageconfigurationpage.py b/bitbake/lib/bb/ui/crumbs/imageconfigurationpage.py new file mode 100644 index 0000000000..2766bea8c7 --- /dev/null +++ b/bitbake/lib/bb/ui/crumbs/imageconfigurationpage.py @@ -0,0 +1,561 @@ +#!/usr/bin/env python +# +# BitBake Graphical GTK User Interface +# +# Copyright (C) 2012 Intel Corporation +# +# Authored by Dongxiao Xu +# Authored by Shane Wang +# +# 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 glib +import re +from bb.ui.crumbs.progressbar import HobProgressBar +from bb.ui.crumbs.hobcolor import HobColors +from bb.ui.crumbs.hobwidget import hic, HobImageButton, HobInfoButton, HobAltButton, HobButton +from bb.ui.crumbs.hoblistmodel import RecipeListModel +from bb.ui.crumbs.hobpages import HobPage +from bb.ui.crumbs.hig.retrieveimagedialog import RetrieveImageDialog + +# +# ImageConfigurationPage +# +class ImageConfigurationPage (HobPage): + + __dummy_machine__ = "--select a machine--" + __dummy_image__ = "--select an image recipe--" + __custom_image__ = "Select from my image recipes" + + def __init__(self, builder): + super(ImageConfigurationPage, self).__init__(builder, "Image configuration") + + self.image_combo_id = None + # we use machine_combo_changed_by_manual to identify the machine is changed by code + # or by manual. If by manual, all user's recipe selection and package selection are + # cleared. + self.machine_combo_changed_by_manual = True + self.stopping = False + self.warning_shift = 0 + self.custom_image_selected = None + self.create_visual_elements() + + def create_visual_elements(self): + # create visual elements + self.toolbar = gtk.Toolbar() + self.toolbar.set_orientation(gtk.ORIENTATION_HORIZONTAL) + self.toolbar.set_style(gtk.TOOLBAR_BOTH) + + my_images_button = self.append_toolbar_button(self.toolbar, + "Images", + hic.ICON_IMAGES_DISPLAY_FILE, + hic.ICON_IMAGES_HOVER_FILE, + "Open previously built images", + self.my_images_button_clicked_cb) + settings_button = self.append_toolbar_button(self.toolbar, + "Settings", + hic.ICON_SETTINGS_DISPLAY_FILE, + hic.ICON_SETTINGS_HOVER_FILE, + "View additional build settings", + self.settings_button_clicked_cb) + + self.config_top_button = self.add_onto_top_bar(self.toolbar) + + self.gtable = gtk.Table(40, 40, True) + self.create_config_machine() + self.create_config_baseimg() + self.config_build_button = self.create_config_build_button() + + def _remove_all_widget(self): + children = self.gtable.get_children() or [] + for child in children: + self.gtable.remove(child) + children = self.box_group_area.get_children() or [] + for child in children: + self.box_group_area.remove(child) + children = self.get_children() or [] + for child in children: + self.remove(child) + + def _pack_components(self, pack_config_build_button = False): + self._remove_all_widget() + self.pack_start(self.config_top_button, expand=False, fill=False) + self.pack_start(self.group_align, expand=True, fill=True) + + self.box_group_area.pack_start(self.gtable, expand=True, fill=True) + if pack_config_build_button: + self.box_group_area.pack_end(self.config_build_button, expand=False, fill=False) + else: + box = gtk.HBox(False, 6) + box.show() + subbox = gtk.HBox(False, 0) + subbox.set_size_request(205, 49) + subbox.show() + box.add(subbox) + self.box_group_area.pack_end(box, False, False) + + def show_machine(self): + self.progress_bar.reset() + self._pack_components(pack_config_build_button = False) + self.set_config_machine_layout(show_progress_bar = False) + self.show_all() + + def update_progress_bar(self, title, fraction, status=None): + if self.stopping == False: + self.progress_bar.update(fraction) + self.progress_bar.set_text(title) + self.progress_bar.set_rcstyle(status) + + def show_info_populating(self): + self._pack_components(pack_config_build_button = False) + self.set_config_machine_layout(show_progress_bar = True) + self.show_all() + + def show_info_populated(self): + self.progress_bar.reset() + self._pack_components(pack_config_build_button = False) + self.set_config_machine_layout(show_progress_bar = False) + self.set_config_baseimg_layout() + self.show_all() + + def show_baseimg_selected(self): + self.progress_bar.reset() + self._pack_components(pack_config_build_button = True) + self.set_config_machine_layout(show_progress_bar = False) + self.set_config_baseimg_layout() + self.show_all() + if self.builder.recipe_model.get_selected_image() == self.builder.recipe_model.__custom_image__: + self.just_bake_button.hide() + + def add_warnings_bar(self): + #create the warnings bar shown when recipes parsing generates warnings + color = HobColors.KHAKI + warnings_bar = gtk.EventBox() + warnings_bar.modify_bg(gtk.STATE_NORMAL, gtk.gdk.color_parse(color)) + warnings_bar.set_flags(gtk.CAN_DEFAULT) + warnings_bar.grab_default() + + build_stop_tab = gtk.Table(10, 20, True) + warnings_bar.add(build_stop_tab) + + icon = gtk.Image() + icon_pix_buffer = gtk.gdk.pixbuf_new_from_file(hic.ICON_INDI_ALERT_FILE) + icon.set_from_pixbuf(icon_pix_buffer) + build_stop_tab.attach(icon, 0, 2, 0, 10) + + label = gtk.Label() + label.set_alignment(0.0, 0.5) + warnings_nb = len(self.builder.parsing_warnings) + if warnings_nb == 1: + label.set_markup("1 recipe parsing warning") + else: + label.set_markup("%s recipe parsing warnings" % warnings_nb) + build_stop_tab.attach(label, 2, 12, 0, 10) + + view_warnings_button = HobButton("View warnings") + view_warnings_button.connect('clicked', self.view_warnings_button_clicked_cb) + build_stop_tab.attach(view_warnings_button, 15, 19, 1, 9) + + return warnings_bar + + def disable_warnings_bar(self): + if self.builder.parsing_warnings: + if hasattr(self, 'warnings_bar'): + self.warnings_bar.hide_all() + self.builder.parsing_warnings = [] + + def create_config_machine(self): + self.machine_title = gtk.Label() + self.machine_title.set_alignment(0.0, 0.5) + mark = "Select a machine" % self.span_tag('x-large', 'bold') + self.machine_title.set_markup(mark) + + self.machine_title_desc = gtk.Label() + self.machine_title_desc.set_alignment(0.0, 0.5) + mark = ("Your selection is the profile of the target machine for which you" + " are building the image.\n") % (self.span_tag('medium')) + self.machine_title_desc.set_markup(mark) + + self.machine_combo = gtk.combo_box_new_text() + self.machine_combo.connect("changed", self.machine_combo_changed_cb) + + icon_file = hic.ICON_LAYERS_DISPLAY_FILE + hover_file = hic.ICON_LAYERS_HOVER_FILE + self.layer_button = HobImageButton("Layers", "Add support for machines, software, etc.", + icon_file, hover_file) + self.layer_button.connect("clicked", self.layer_button_clicked_cb) + + markup = "Layers are a powerful mechanism to extend the Yocto Project " + markup += "with your own functionality.\n" + markup += "For more on layers, check the reference manual." + self.layer_info_icon = HobInfoButton("Layers" + "*" + markup, self.get_parent()) + self.progress_bar = HobProgressBar() + self.stop_button = HobAltButton("Stop") + self.stop_button.connect("clicked", self.stop_button_clicked_cb) + self.machine_separator = gtk.HSeparator() + + def set_config_machine_layout(self, show_progress_bar = False): + self.gtable.attach(self.machine_title, 0, 40, 0, 4) + self.gtable.attach(self.machine_title_desc, 0, 40, 4, 6) + self.gtable.attach(self.machine_combo, 0, 12, 7, 10) + self.gtable.attach(self.layer_button, 14, 36, 7, 12) + self.gtable.attach(self.layer_info_icon, 36, 40, 7, 11) + if show_progress_bar: + #self.gtable.attach(self.progress_box, 0, 40, 15, 18) + self.gtable.attach(self.progress_bar, 0, 37, 15, 18) + self.gtable.attach(self.stop_button, 37, 40, 15, 18, 0, 0) + if self.builder.parsing_warnings: + self.warnings_bar = self.add_warnings_bar() + self.gtable.attach(self.warnings_bar, 0, 40, 14, 18) + self.warning_shift = 4 + else: + self.warning_shift = 0 + self.gtable.attach(self.machine_separator, 0, 40, 13, 14) + + def create_config_baseimg(self): + self.image_title = gtk.Label() + self.image_title.set_alignment(0, 1.0) + mark = "Select an image recipe" % self.span_tag('x-large', 'bold') + self.image_title.set_markup(mark) + + self.image_title_desc = gtk.Label() + self.image_title_desc.set_alignment(0, 0.5) + + mark = ("Image recipes are a starting point for the type of image you want. " + "You can build them as \n" + "they are or edit them to suit your needs.\n") % self.span_tag('medium') + self.image_title_desc.set_markup(mark) + + self.image_combo = gtk.combo_box_new_text() + self.image_combo.set_row_separator_func(self.combo_separator_func, None) + self.image_combo_id = self.image_combo.connect("changed", self.image_combo_changed_cb) + + self.image_desc = gtk.Label() + self.image_desc.set_alignment(0.0, 0.5) + self.image_desc.set_size_request(256, -1) + self.image_desc.set_justify(gtk.JUSTIFY_LEFT) + self.image_desc.set_line_wrap(True) + + # button to view recipes + icon_file = hic.ICON_RCIPE_DISPLAY_FILE + hover_file = hic.ICON_RCIPE_HOVER_FILE + self.view_adv_configuration_button = HobImageButton("Advanced configuration", + "Select image types, package formats, etc", + icon_file, hover_file) + self.view_adv_configuration_button.connect("clicked", self.view_adv_configuration_button_clicked_cb) + + self.image_separator = gtk.HSeparator() + + def combo_separator_func(self, model, iter, user_data): + name = model.get_value(iter, 0) + if name == "--Separator--": + return True + + def set_config_baseimg_layout(self): + self.gtable.attach(self.image_title, 0, 40, 15+self.warning_shift, 17+self.warning_shift) + self.gtable.attach(self.image_title_desc, 0, 40, 18+self.warning_shift, 22+self.warning_shift) + self.gtable.attach(self.image_combo, 0, 12, 23+self.warning_shift, 26+self.warning_shift) + self.gtable.attach(self.image_desc, 0, 12, 27+self.warning_shift, 33+self.warning_shift) + self.gtable.attach(self.view_adv_configuration_button, 14, 36, 23+self.warning_shift, 28+self.warning_shift) + self.gtable.attach(self.image_separator, 0, 40, 35+self.warning_shift, 36+self.warning_shift) + + def create_config_build_button(self): + # Create the "Build packages" and "Build image" buttons at the bottom + button_box = gtk.HBox(False, 6) + + # create button "Build image" + self.just_bake_button = HobButton("Build image") + self.just_bake_button.set_tooltip_text("Build the image recipe as it is") + self.just_bake_button.connect("clicked", self.just_bake_button_clicked_cb) + button_box.pack_end(self.just_bake_button, expand=False, fill=False) + + # create button "Edit image recipe" + self.edit_image_button = HobAltButton("Edit image recipe") + self.edit_image_button.set_tooltip_text("Customize the recipes and packages to be included in your image") + self.edit_image_button.connect("clicked", self.edit_image_button_clicked_cb) + button_box.pack_end(self.edit_image_button, expand=False, fill=False) + + return button_box + + def stop_button_clicked_cb(self, button): + self.stopping = True + self.progress_bar.set_text("Stopping recipe parsing") + self.progress_bar.set_rcstyle("stop") + self.builder.cancel_parse_sync() + + def view_warnings_button_clicked_cb(self, button): + self.builder.show_warning_dialog() + + def machine_combo_changed_idle_cb(self): + self.builder.window.set_cursor(None) + + def machine_combo_changed_cb(self, machine_combo): + self.stopping = False + self.builder.parsing_warnings = [] + combo_item = machine_combo.get_active_text() + if not combo_item or combo_item == self.__dummy_machine__: + return + + self.builder.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.WATCH)) + self.builder.wait(0.1) #wait for combo and cursor to update + + # remove __dummy_machine__ item from the store list after first user selection + # because it is no longer valid + combo_store = machine_combo.get_model() + if len(combo_store) and (combo_store[0][0] == self.__dummy_machine__): + machine_combo.remove_text(0) + + self.builder.configuration.curr_mach = combo_item + if self.machine_combo_changed_by_manual: + self.builder.configuration.clear_selection() + # reset machine_combo_changed_by_manual + self.machine_combo_changed_by_manual = True + + self.builder.configuration.selected_image = None + + # Do reparse recipes + self.builder.populate_recipe_package_info_async() + + glib.idle_add(self.machine_combo_changed_idle_cb) + + def update_machine_combo(self): + self.disable_warnings_bar() + all_machines = [self.__dummy_machine__] + self.builder.parameters.all_machines + + model = self.machine_combo.get_model() + model.clear() + for machine in all_machines: + self.machine_combo.append_text(machine) + self.machine_combo.set_active(0) + + def switch_machine_combo(self): + self.disable_warnings_bar() + self.machine_combo_changed_by_manual = False + model = self.machine_combo.get_model() + active = 0 + while active < len(model): + if model[active][0] == self.builder.configuration.curr_mach: + self.machine_combo.set_active(active) + return + active += 1 + + if model[0][0] != self.__dummy_machine__: + self.machine_combo.insert_text(0, self.__dummy_machine__) + + self.machine_combo.set_active(0) + + def update_image_desc(self): + desc = "" + selected_image = self.image_combo.get_active_text() + if selected_image and selected_image in self.builder.recipe_model.pn_path.keys(): + image_path = self.builder.recipe_model.pn_path[selected_image] + image_iter = self.builder.recipe_model.get_iter(image_path) + desc = self.builder.recipe_model.get_value(image_iter, self.builder.recipe_model.COL_DESC) + + mark = ("%s\n") % (self.span_tag('small'), desc) + self.image_desc.set_markup(mark) + + def image_combo_changed_idle_cb(self, selected_image, selected_recipes, selected_packages): + self.builder.update_recipe_model(selected_image, selected_recipes) + self.builder.update_package_model(selected_packages) + self.builder.window_sensitive(True) + + def image_combo_changed_cb(self, combo): + self.builder.window_sensitive(False) + selected_image = self.image_combo.get_active_text() + if selected_image == self.__custom_image__: + topdir = self.builder.get_topdir() + images_dir = topdir + "/recipes/images/custom/" + self.builder.ensure_dir(images_dir) + + dialog = RetrieveImageDialog(images_dir, "Select from my image recipes", + self.builder, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT) + response = dialog.run() + if response == gtk.RESPONSE_OK: + image_name = dialog.get_filename() + head, tail = os.path.split(image_name) + selected_image = os.path.splitext(tail)[0] + self.custom_image_selected = selected_image + self.update_image_combo(self.builder.recipe_model, selected_image) + else: + selected_image = self.__dummy_image__ + self.update_image_combo(self.builder.recipe_model, None) + dialog.destroy() + else: + if self.custom_image_selected: + self.custom_image_selected = None + self.update_image_combo(self.builder.recipe_model, selected_image) + + if not selected_image or (selected_image == self.__dummy_image__): + self.builder.window_sensitive(True) + self.just_bake_button.hide() + self.edit_image_button.hide() + return + + # remove __dummy_image__ item from the store list after first user selection + # because it is no longer valid + combo_store = combo.get_model() + if len(combo_store) and (combo_store[0][0] == self.__dummy_image__): + combo.remove_text(0) + + self.builder.customized = False + + selected_recipes = [] + + image_path = self.builder.recipe_model.pn_path[selected_image] + image_iter = self.builder.recipe_model.get_iter(image_path) + selected_packages = self.builder.recipe_model.get_value(image_iter, self.builder.recipe_model.COL_INSTALL).split() + self.update_image_desc() + + self.builder.recipe_model.reset() + self.builder.package_model.reset() + + self.show_baseimg_selected() + + if selected_image == self.builder.recipe_model.__custom_image__: + self.just_bake_button.hide() + + glib.idle_add(self.image_combo_changed_idle_cb, selected_image, selected_recipes, selected_packages) + + def _image_combo_connect_signal(self): + if not self.image_combo_id: + self.image_combo_id = self.image_combo.connect("changed", self.image_combo_changed_cb) + + def _image_combo_disconnect_signal(self): + if self.image_combo_id: + self.image_combo.disconnect(self.image_combo_id) + self.image_combo_id = None + + def update_image_combo(self, recipe_model, selected_image): + # Update the image combo according to the images in the recipe_model + # populate image combo + filter = {RecipeListModel.COL_TYPE : ['image']} + image_model = recipe_model.tree_model(filter) + image_model.set_sort_column_id(recipe_model.COL_NAME, gtk.SORT_ASCENDING) + active = 0 + cnt = 0 + + white_pattern = [] + if self.builder.parameters.image_white_pattern: + for i in self.builder.parameters.image_white_pattern.split(): + white_pattern.append(re.compile(i)) + + black_pattern = [] + if self.builder.parameters.image_black_pattern: + for i in self.builder.parameters.image_black_pattern.split(): + black_pattern.append(re.compile(i)) + black_pattern.append(re.compile("hob-image")) + black_pattern.append(re.compile("edited(-[0-9]*)*.bb$")) + + it = image_model.get_iter_first() + self._image_combo_disconnect_signal() + model = self.image_combo.get_model() + model.clear() + # Set a indicator text to combo store when first open + if not selected_image: + self.image_combo.append_text(self.__dummy_image__) + cnt = cnt + 1 + + self.image_combo.append_text(self.__custom_image__) + self.image_combo.append_text("--Separator--") + cnt = cnt + 2 + + topdir = self.builder.get_topdir() + # append and set active + while it: + path = image_model.get_path(it) + it = image_model.iter_next(it) + image_name = image_model[path][recipe_model.COL_NAME] + if image_name == self.builder.recipe_model.__custom_image__: + continue + + if black_pattern: + allow = True + for pattern in black_pattern: + if pattern.search(image_name): + allow = False + break + elif white_pattern: + allow = False + for pattern in white_pattern: + if pattern.search(image_name): + allow = True + break + else: + allow = True + + file_name = image_model[path][recipe_model.COL_FILE] + if file_name and topdir in file_name: + allow = False + + if allow: + self.image_combo.append_text(image_name) + if image_name == selected_image: + active = cnt + cnt = cnt + 1 + self.image_combo.append_text(self.builder.recipe_model.__custom_image__) + + if selected_image == self.builder.recipe_model.__custom_image__: + active = cnt + + if self.custom_image_selected: + self.image_combo.append_text("--Separator--") + self.image_combo.append_text(self.custom_image_selected) + cnt = cnt + 2 + if self.custom_image_selected == selected_image: + active = cnt + + self.image_combo.set_active(active) + + if active != 0: + self.show_baseimg_selected() + + self._image_combo_connect_signal() + + def layer_button_clicked_cb(self, button): + # Create a layer selection dialog + self.builder.show_layer_selection_dialog() + + def view_adv_configuration_button_clicked_cb(self, button): + # Create an advanced settings dialog + response, settings_changed = self.builder.show_adv_settings_dialog() + if not response: + return + if settings_changed: + self.builder.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.WATCH)) + self.builder.wait(0.1) #wait for adv_settings_dialog to terminate + self.builder.reparse_post_adv_settings() + self.builder.window.set_cursor(None) + + def just_bake_button_clicked_cb(self, button): + self.builder.parsing_warnings = [] + self.builder.just_bake() + + def edit_image_button_clicked_cb(self, button): + self.builder.set_base_image() + self.builder.show_recipes() + + def my_images_button_clicked_cb(self, button): + self.builder.show_load_my_images_dialog() + + def settings_button_clicked_cb(self, button): + # Create an advanced settings dialog + response, settings_changed = self.builder.show_simple_settings_dialog() + if not response: + return + if settings_changed: + self.builder.reparse_post_adv_settings() diff --git a/bitbake/lib/bb/ui/crumbs/imagedetailspage.py b/bitbake/lib/bb/ui/crumbs/imagedetailspage.py new file mode 100755 index 0000000000..352e9489fd --- /dev/null +++ b/bitbake/lib/bb/ui/crumbs/imagedetailspage.py @@ -0,0 +1,669 @@ +#!/usr/bin/env python +# +# BitBake Graphical GTK User Interface +# +# Copyright (C) 2012 Intel Corporation +# +# Authored by Dongxiao Xu +# Authored by Shane Wang +# +# 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 gobject +import gtk +from bb.ui.crumbs.hobcolor import HobColors +from bb.ui.crumbs.hobwidget import hic, HobViewTable, HobAltButton, HobButton +from bb.ui.crumbs.hobpages import HobPage +import subprocess +from bb.ui.crumbs.hig.crumbsdialog import CrumbsDialog +from bb.ui.crumbs.hig.saveimagedialog import SaveImageDialog + +# +# ImageDetailsPage +# +class ImageDetailsPage (HobPage): + + class DetailBox (gtk.EventBox): + def __init__(self, widget = None, varlist = None, vallist = None, icon = None, button = None, button2=None, color = HobColors.LIGHT_GRAY): + gtk.EventBox.__init__(self) + + # set color + style = self.get_style().copy() + style.bg[gtk.STATE_NORMAL] = self.get_colormap().alloc_color(color, False, False) + self.set_style(style) + + self.row = gtk.Table(1, 2, False) + self.row.set_border_width(10) + self.add(self.row) + + total_rows = 0 + if widget: + total_rows = 10 + if varlist and vallist: + # pack the icon and the text on the left + total_rows += len(varlist) + self.table = gtk.Table(total_rows, 20, True) + self.table.set_row_spacings(6) + self.table.set_size_request(100, -1) + self.row.attach(self.table, 0, 1, 0, 1, xoptions=gtk.FILL|gtk.EXPAND, yoptions=gtk.FILL) + + colid = 0 + rowid = 0 + self.line_widgets = {} + if icon: + self.table.attach(icon, colid, colid + 2, 0, 1) + colid = colid + 2 + if widget: + self.table.attach(widget, colid, 20, 0, 10) + rowid = 10 + if varlist and vallist: + for row in range(rowid, total_rows): + index = row - rowid + self.line_widgets[varlist[index]] = self.text2label(varlist[index], vallist[index]) + self.table.attach(self.line_widgets[varlist[index]], colid, 20, row, row + 1) + # pack the button on the right + if button: + self.bbox = gtk.VBox() + self.bbox.pack_start(button, expand=True, fill=False) + if button2: + self.bbox.pack_start(button2, expand=True, fill=False) + self.bbox.set_size_request(150,-1) + self.row.attach(self.bbox, 1, 2, 0, 1, xoptions=gtk.FILL, yoptions=gtk.EXPAND) + + def update_line_widgets(self, variable, value): + if len(self.line_widgets) == 0: + return + if not isinstance(self.line_widgets[variable], gtk.Label): + return + self.line_widgets[variable].set_markup(self.format_line(variable, value)) + + def wrap_line(self, inputs): + # wrap the long text of inputs + wrap_width_chars = 75 + outputs = "" + tmps = inputs + less_chars = len(inputs) + while (less_chars - wrap_width_chars) > 0: + less_chars -= wrap_width_chars + outputs += tmps[:wrap_width_chars] + "\n " + tmps = inputs[less_chars:] + outputs += tmps + return outputs + + def format_line(self, variable, value): + wraped_value = self.wrap_line(value) + markup = "%s" % variable + markup += "%s" % wraped_value + return markup + + def text2label(self, variable, value): + # append the name:value to the left box + # such as "Name: hob-core-minimal-variant-2011-12-15-beagleboard" + label = gtk.Label() + label.set_alignment(0.0, 0.5) + label.set_markup(self.format_line(variable, value)) + return label + + class BuildDetailBox (gtk.EventBox): + def __init__(self, varlist = None, vallist = None, icon = None, color = HobColors.LIGHT_GRAY): + gtk.EventBox.__init__(self) + + # set color + style = self.get_style().copy() + style.bg[gtk.STATE_NORMAL] = self.get_colormap().alloc_color(color, False, False) + self.set_style(style) + + self.hbox = gtk.HBox() + self.hbox.set_border_width(10) + self.add(self.hbox) + + total_rows = 0 + if varlist and vallist: + # pack the icon and the text on the left + total_rows += len(varlist) + self.table = gtk.Table(total_rows, 20, True) + self.table.set_row_spacings(6) + self.table.set_size_request(100, -1) + self.hbox.pack_start(self.table, expand=True, fill=True, padding=15) + + colid = 0 + rowid = 0 + self.line_widgets = {} + if icon: + self.table.attach(icon, colid, colid + 2, 0, 1) + colid = colid + 2 + if varlist and vallist: + for row in range(rowid, total_rows): + index = row - rowid + self.line_widgets[varlist[index]] = self.text2label(varlist[index], vallist[index]) + self.table.attach(self.line_widgets[varlist[index]], colid, 20, row, row + 1) + + def update_line_widgets(self, variable, value): + if len(self.line_widgets) == 0: + return + if not isinstance(self.line_widgets[variable], gtk.Label): + return + self.line_widgets[variable].set_markup(self.format_line(variable, value)) + + def wrap_line(self, inputs): + # wrap the long text of inputs + wrap_width_chars = 75 + outputs = "" + tmps = inputs + less_chars = len(inputs) + while (less_chars - wrap_width_chars) > 0: + less_chars -= wrap_width_chars + outputs += tmps[:wrap_width_chars] + "\n " + tmps = inputs[less_chars:] + outputs += tmps + return outputs + + def format_line(self, variable, value): + wraped_value = self.wrap_line(value) + markup = "%s" % variable + markup += "%s" % wraped_value + return markup + + def text2label(self, variable, value): + # append the name:value to the left box + # such as "Name: hob-core-minimal-variant-2011-12-15-beagleboard" + label = gtk.Label() + label.set_alignment(0.0, 0.5) + label.set_markup(self.format_line(variable, value)) + return label + + def __init__(self, builder): + super(ImageDetailsPage, self).__init__(builder, "Image details") + + self.image_store = [] + self.button_ids = {} + self.details_bottom_buttons = gtk.HBox(False, 6) + self.image_saved = False + self.create_visual_elements() + self.name_field_template = "" + self.description_field_template = "" + + def create_visual_elements(self): + # create visual elements + # create the toolbar + self.toolbar = gtk.Toolbar() + self.toolbar.set_orientation(gtk.ORIENTATION_HORIZONTAL) + self.toolbar.set_style(gtk.TOOLBAR_BOTH) + + my_images_button = self.append_toolbar_button(self.toolbar, + "Images", + hic.ICON_IMAGES_DISPLAY_FILE, + hic.ICON_IMAGES_HOVER_FILE, + "Open previously built images", + self.my_images_button_clicked_cb) + settings_button = self.append_toolbar_button(self.toolbar, + "Settings", + hic.ICON_SETTINGS_DISPLAY_FILE, + hic.ICON_SETTINGS_HOVER_FILE, + "View additional build settings", + self.settings_button_clicked_cb) + + self.details_top_buttons = self.add_onto_top_bar(self.toolbar) + + def _remove_all_widget(self): + children = self.get_children() or [] + for child in children: + self.remove(child) + children = self.box_group_area.get_children() or [] + for child in children: + self.box_group_area.remove(child) + children = self.details_bottom_buttons.get_children() or [] + for child in children: + self.details_bottom_buttons.remove(child) + + def show_page(self, step): + self.build_succeeded = (step == self.builder.IMAGE_GENERATED) + image_addr = self.builder.parameters.image_addr + image_names = self.builder.parameters.image_names + if self.build_succeeded: + machine = self.builder.configuration.curr_mach + base_image = self.builder.recipe_model.get_selected_image() + layers = self.builder.configuration.layers + pkg_num = "%s" % len(self.builder.package_model.get_selected_packages()) + log_file = self.builder.current_logfile + else: + pkg_num = "N/A" + log_file = None + + # remove + for button_id, button in self.button_ids.items(): + button.disconnect(button_id) + self._remove_all_widget() + + # repack + self.pack_start(self.details_top_buttons, expand=False, fill=False) + self.pack_start(self.group_align, expand=True, fill=True) + + self.build_result = None + if self.image_saved or (self.build_succeeded and self.builder.current_step == self.builder.IMAGE_GENERATING): + # building is the previous step + icon = gtk.Image() + pixmap_path = hic.ICON_INDI_CONFIRM_FILE + color = HobColors.RUNNING + pix_buffer = gtk.gdk.pixbuf_new_from_file(pixmap_path) + icon.set_from_pixbuf(pix_buffer) + varlist = [""] + if self.image_saved: + vallist = ["Your image recipe has been saved"] + else: + vallist = ["Your image is ready"] + self.build_result = self.BuildDetailBox(varlist=varlist, vallist=vallist, icon=icon, color=color) + self.box_group_area.pack_start(self.build_result, expand=False, fill=False) + + self.buttonlist = ["Build new image", "Save image recipe", "Run image", "Deploy image"] + + # Name + self.image_store = [] + self.toggled_image = "" + default_image_size = 0 + self.num_toggled = 0 + i = 0 + for image_name in image_names: + image_size = HobPage._size_to_string(os.stat(os.path.join(image_addr, image_name)).st_size) + + image_attr = ("run" if (self.test_type_runnable(image_name) and self.test_mach_runnable(image_name)) else \ + ("deploy" if self.test_deployable(image_name) else "")) + is_toggled = (image_attr != "") + + if not self.toggled_image: + if i == (len(image_names) - 1): + is_toggled = True + if is_toggled: + default_image_size = image_size + self.toggled_image = image_name + + split_stuff = image_name.split('.') + if "rootfs" in split_stuff: + image_type = image_name[(len(split_stuff[0]) + len(".rootfs") + 1):] + else: + image_type = image_name[(len(split_stuff[0]) + 1):] + + self.image_store.append({'name': image_name, + 'type': image_type, + 'size': image_size, + 'is_toggled': is_toggled, + 'action_attr': image_attr,}) + + i = i + 1 + self.num_toggled += is_toggled + + is_runnable = self.create_bottom_buttons(self.buttonlist, self.toggled_image) + + # Generated image files info + varlist = ["Name: ", "Files created: ", "Directory: "] + vallist = [] + + vallist.append(image_name.split('.')[0]) + vallist.append(', '.join(fileitem['type'] for fileitem in self.image_store)) + vallist.append(image_addr) + + view_files_button = HobAltButton("View files") + view_files_button.connect("clicked", self.view_files_clicked_cb, image_addr) + view_files_button.set_tooltip_text("Open the directory containing the image files") + open_log_button = None + if log_file: + open_log_button = HobAltButton("Open log") + open_log_button.connect("clicked", self.open_log_clicked_cb, log_file) + open_log_button.set_tooltip_text("Open the build's log file") + self.image_detail = self.DetailBox(varlist=varlist, vallist=vallist, button=view_files_button, button2=open_log_button) + self.box_group_area.pack_start(self.image_detail, expand=False, fill=True) + + # The default kernel box for the qemu images + self.sel_kernel = "" + self.kernel_detail = None + if 'qemu' in image_name: + self.sel_kernel = self.get_kernel_file_name() + + # varlist = ["Kernel: "] + # vallist = [] + # vallist.append(self.sel_kernel) + + # change_kernel_button = HobAltButton("Change") + # change_kernel_button.connect("clicked", self.change_kernel_cb) + # change_kernel_button.set_tooltip_text("Change qemu kernel file") + # self.kernel_detail = self.DetailBox(varlist=varlist, vallist=vallist, button=change_kernel_button) + # self.box_group_area.pack_start(self.kernel_detail, expand=True, fill=True) + + # Machine, Image recipe and Layers + layer_num_limit = 15 + varlist = ["Machine: ", "Image recipe: ", "Layers: "] + vallist = [] + self.setting_detail = None + if self.build_succeeded: + vallist.append(machine) + if self.builder.recipe_model.is_custom_image(): + if self.builder.configuration.initial_selected_image == self.builder.recipe_model.__custom_image__: + base_image ="New image recipe" + else: + base_image = self.builder.configuration.initial_selected_image + " (edited)" + vallist.append(base_image) + i = 0 + for layer in layers: + if i > layer_num_limit: + break + varlist.append(" - ") + i += 1 + vallist.append("") + i = 0 + for layer in layers: + if i > layer_num_limit: + break + elif i == layer_num_limit: + vallist.append("and more...") + else: + vallist.append(layer) + i += 1 + + edit_config_button = HobAltButton("Edit configuration") + edit_config_button.set_tooltip_text("Edit machine and image recipe") + edit_config_button.connect("clicked", self.edit_config_button_clicked_cb) + self.setting_detail = self.DetailBox(varlist=varlist, vallist=vallist, button=edit_config_button) + self.box_group_area.pack_start(self.setting_detail, expand=True, fill=True) + + # Packages included, and Total image size + varlist = ["Packages included: ", "Total image size: "] + vallist = [] + vallist.append(pkg_num) + vallist.append(default_image_size) + self.builder.configuration.image_size = default_image_size + self.builder.configuration.image_packages = self.builder.configuration.selected_packages + if self.build_succeeded: + edit_packages_button = HobAltButton("Edit packages") + edit_packages_button.set_tooltip_text("Edit the packages included in your image") + edit_packages_button.connect("clicked", self.edit_packages_button_clicked_cb) + else: # get to this page from "My images" + edit_packages_button = None + self.package_detail = self.DetailBox(varlist=varlist, vallist=vallist, button=edit_packages_button) + self.box_group_area.pack_start(self.package_detail, expand=True, fill=True) + + # pack the buttons at the bottom, at this time they are already created. + if self.build_succeeded: + self.box_group_area.pack_end(self.details_bottom_buttons, expand=False, fill=False) + else: # for "My images" page + self.details_separator = gtk.HSeparator() + self.box_group_area.pack_start(self.details_separator, expand=False, fill=False) + self.box_group_area.pack_start(self.details_bottom_buttons, expand=False, fill=False) + + self.show_all() + if self.kernel_detail and (not is_runnable): + self.kernel_detail.hide() + self.image_saved = False + + def view_files_clicked_cb(self, button, image_addr): + subprocess.call("xdg-open /%s" % image_addr, shell=True) + + def open_log_clicked_cb(self, button, log_file): + if log_file: + log_file = "file:///" + log_file + gtk.show_uri(screen=button.get_screen(), uri=log_file, timestamp=0) + + def refresh_package_detail_box(self, image_size): + self.package_detail.update_line_widgets("Total image size: ", image_size) + + def test_type_runnable(self, image_name): + type_runnable = False + for t in self.builder.parameters.runnable_image_types: + if image_name.endswith(t): + type_runnable = True + break + return type_runnable + + def test_mach_runnable(self, image_name): + mach_runnable = False + for t in self.builder.parameters.runnable_machine_patterns: + if t in image_name: + mach_runnable = True + break + return mach_runnable + + def test_deployable(self, image_name): + if self.builder.configuration.curr_mach.startswith("qemu"): + return False + deployable = False + for t in self.builder.parameters.deployable_image_types: + if image_name.endswith(t): + deployable = True + break + return deployable + + def get_kernel_file_name(self, kernel_addr=""): + kernel_name = "" + + if not kernel_addr: + kernel_addr = self.builder.parameters.image_addr + + files = [f for f in os.listdir(kernel_addr) if f[0] <> '.'] + for check_file in files: + if check_file.endswith(".bin"): + name_splits = check_file.split(".")[0] + if self.builder.parameters.kernel_image_type in name_splits.split("-"): + kernel_name = check_file + break + + return kernel_name + + def show_builded_images_dialog(self, widget, primary_action=""): + title = primary_action if primary_action else "Your builded images" + dialog = CrumbsDialog(title, self.builder, + gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT) + dialog.set_border_width(12) + + label = gtk.Label() + label.set_use_markup(True) + label.set_alignment(0.0, 0.5) + label.set_padding(12,0) + if primary_action == "Run image": + label.set_markup("Select the image file you want to run:") + elif primary_action == "Deploy image": + label.set_markup("Select the image file you want to deploy:") + else: + label.set_markup("Select the image file you want to %s" % primary_action) + dialog.vbox.pack_start(label, expand=False, fill=False) + + # filter created images as action attribution (deploy or run) + action_attr = "" + action_images = [] + for fileitem in self.image_store: + action_attr = fileitem['action_attr'] + if (action_attr == 'run' and primary_action == "Run image") \ + or (action_attr == 'deploy' and primary_action == "Deploy image"): + action_images.append(fileitem) + + # pack the corresponding 'runnable' or 'deploy' radio_buttons, if there has no more than one file. + # assume that there does not both have 'deploy' and 'runnable' files in the same building result + # in possible as design. + curr_row = 0 + rows = (len(action_images)) if len(action_images) < 10 else 10 + table = gtk.Table(rows, 10, True) + table.set_row_spacings(6) + table.set_col_spacing(0, 12) + table.set_col_spacing(5, 12) + + sel_parent_btn = None + for fileitem in action_images: + sel_btn = gtk.RadioButton(sel_parent_btn, fileitem['type']) + sel_parent_btn = sel_btn if not sel_parent_btn else sel_parent_btn + sel_btn.set_active(fileitem['is_toggled']) + sel_btn.connect('toggled', self.table_selected_cb, fileitem) + if curr_row < 10: + table.attach(sel_btn, 0, 4, curr_row, curr_row + 1, xpadding=24) + else: + table.attach(sel_btn, 5, 9, curr_row - 10, curr_row - 9, xpadding=24) + curr_row += 1 + + dialog.vbox.pack_start(table, expand=False, fill=False, padding=6) + + button = dialog.add_button("Cancel", gtk.RESPONSE_CANCEL) + HobAltButton.style_button(button) + + if primary_action: + button = dialog.add_button(primary_action, gtk.RESPONSE_YES) + HobButton.style_button(button) + + dialog.show_all() + + response = dialog.run() + dialog.destroy() + + if response != gtk.RESPONSE_YES: + return + + for fileitem in self.image_store: + if fileitem['is_toggled']: + if fileitem['action_attr'] == 'run': + self.builder.runqemu_image(fileitem['name'], self.sel_kernel) + elif fileitem['action_attr'] == 'deploy': + self.builder.deploy_image(fileitem['name']) + + def table_selected_cb(self, tbutton, image): + image['is_toggled'] = tbutton.get_active() + if image['is_toggled']: + self.toggled_image = image['name'] + + def change_kernel_cb(self, widget): + kernel_path = self.builder.show_load_kernel_dialog() + if kernel_path and self.kernel_detail: + import os.path + self.sel_kernel = os.path.basename(kernel_path) + markup = self.kernel_detail.format_line("Kernel: ", self.sel_kernel) + label = ((self.kernel_detail.get_children()[0]).get_children()[0]).get_children()[0] + label.set_markup(markup) + + def create_bottom_buttons(self, buttonlist, image_name): + # Create the buttons at the bottom + created = False + packed = False + self.button_ids = {} + is_runnable = False + + # create button "Deploy image" + name = "Deploy image" + if name in buttonlist and self.test_deployable(image_name): + deploy_button = HobButton('Deploy image') + #deploy_button.set_size_request(205, 49) + deploy_button.set_tooltip_text("Burn a live image to a USB drive or flash memory") + deploy_button.set_flags(gtk.CAN_DEFAULT) + button_id = deploy_button.connect("clicked", self.deploy_button_clicked_cb) + self.button_ids[button_id] = deploy_button + self.details_bottom_buttons.pack_end(deploy_button, expand=False, fill=False) + created = True + packed = True + + name = "Run image" + if name in buttonlist and self.test_type_runnable(image_name) and self.test_mach_runnable(image_name): + if created == True: + # separator + #label = gtk.Label(" or ") + #self.details_bottom_buttons.pack_end(label, expand=False, fill=False) + + # create button "Run image" + run_button = HobAltButton("Run image") + else: + # create button "Run image" as the primary button + run_button = HobButton("Run image") + #run_button.set_size_request(205, 49) + run_button.set_flags(gtk.CAN_DEFAULT) + packed = True + run_button.set_tooltip_text("Start up an image with qemu emulator") + button_id = run_button.connect("clicked", self.run_button_clicked_cb) + self.button_ids[button_id] = run_button + self.details_bottom_buttons.pack_end(run_button, expand=False, fill=False) + created = True + is_runnable = True + + name = "Save image recipe" + if name in buttonlist and self.builder.recipe_model.is_custom_image(): + save_button = HobAltButton("Save image recipe") + save_button.set_tooltip_text("Keep your changes saving them as an image recipe") + save_button.set_sensitive(not self.image_saved) + button_id = save_button.connect("clicked", self.save_button_clicked_cb) + self.button_ids[button_id] = save_button + self.details_bottom_buttons.pack_end(save_button, expand=False, fill=False) + + name = "Build new image" + if name in buttonlist: + # create button "Build new image" + if packed: + build_new_button = HobAltButton("Build new image") + else: + build_new_button = HobButton("Build new image") + build_new_button.set_flags(gtk.CAN_DEFAULT) + #build_new_button.set_size_request(205, 49) + self.details_bottom_buttons.pack_end(build_new_button, expand=False, fill=False) + build_new_button.set_tooltip_text("Create a new image from scratch") + button_id = build_new_button.connect("clicked", self.build_new_button_clicked_cb) + self.button_ids[button_id] = build_new_button + + return is_runnable + + def deploy_button_clicked_cb(self, button): + if self.toggled_image: + if self.num_toggled > 1: + self.set_sensitive(False) + self.show_builded_images_dialog(None, "Deploy image") + self.set_sensitive(True) + else: + self.builder.deploy_image(self.toggled_image) + + def run_button_clicked_cb(self, button): + if self.toggled_image: + if self.num_toggled > 1: + self.set_sensitive(False) + self.show_builded_images_dialog(None, "Run image") + self.set_sensitive(True) + else: + self.builder.runqemu_image(self.toggled_image, self.sel_kernel) + + def save_button_clicked_cb(self, button): + topdir = self.builder.get_topdir() + images_dir = topdir + "/recipes/images/custom/" + self.builder.ensure_dir(images_dir) + + self.name_field_template = self.builder.image_configuration_page.custom_image_selected + if self.name_field_template: + image_path = self.builder.recipe_model.pn_path[self.name_field_template] + image_iter = self.builder.recipe_model.get_iter(image_path) + self.description_field_template = self.builder.recipe_model.get_value(image_iter, self.builder.recipe_model.COL_DESC) + else: + self.name_field_template = "" + + dialog = SaveImageDialog(images_dir, self.name_field_template, self.description_field_template, + "Save image recipe", self.builder, gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT) + response = dialog.run() + dialog.destroy() + + def build_new_button_clicked_cb(self, button): + self.builder.initiate_new_build_async() + + def edit_config_button_clicked_cb(self, button): + self.builder.show_configuration() + + def edit_packages_button_clicked_cb(self, button): + self.builder.show_packages() + + def my_images_button_clicked_cb(self, button): + self.builder.show_load_my_images_dialog() + + def settings_button_clicked_cb(self, button): + # Create an advanced settings dialog + response, settings_changed = self.builder.show_simple_settings_dialog() + if not response: + return + if settings_changed: + self.builder.reparse_post_adv_settings() diff --git a/bitbake/lib/bb/ui/crumbs/packageselectionpage.py b/bitbake/lib/bb/ui/crumbs/packageselectionpage.py new file mode 100755 index 0000000000..7c62b36e6b --- /dev/null +++ b/bitbake/lib/bb/ui/crumbs/packageselectionpage.py @@ -0,0 +1,355 @@ +#!/usr/bin/env python +# +# BitBake Graphical GTK User Interface +# +# Copyright (C) 2012 Intel Corporation +# +# Authored by Dongxiao Xu +# Authored by Shane Wang +# +# 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 glib +from bb.ui.crumbs.hobcolor import HobColors +from bb.ui.crumbs.hobwidget import HobViewTable, HobNotebook, HobAltButton, HobButton +from bb.ui.crumbs.hoblistmodel import PackageListModel +from bb.ui.crumbs.hobpages import HobPage + +# +# PackageSelectionPage +# +class PackageSelectionPage (HobPage): + + pages = [ + { + 'name' : 'Included packages', + 'tooltip' : 'The packages currently included for your image', + 'filter' : { PackageListModel.COL_INC : [True] }, + 'search' : 'Search packages by name', + 'searchtip' : 'Enter a package name to find it', + 'columns' : [{ + 'col_name' : 'Package name', + 'col_id' : PackageListModel.COL_NAME, + 'col_style': 'text', + 'col_min' : 100, + 'col_max' : 300, + 'expand' : 'True' + }, { + 'col_name' : 'Size', + 'col_id' : PackageListModel.COL_SIZE, + 'col_style': 'text', + 'col_min' : 100, + 'col_max' : 300, + 'expand' : 'True' + }, { + 'col_name' : 'Recipe', + 'col_id' : PackageListModel.COL_RCP, + 'col_style': 'text', + 'col_min' : 100, + 'col_max' : 250, + 'expand' : 'True' + }, { + 'col_name' : 'Brought in by (+others)', + 'col_id' : PackageListModel.COL_BINB, + 'col_style': 'binb', + 'col_min' : 100, + 'col_max' : 350, + 'expand' : 'True' + }, { + 'col_name' : 'Included', + 'col_id' : PackageListModel.COL_INC, + 'col_style': 'check toggle', + 'col_min' : 100, + 'col_max' : 100 + }] + }, { + 'name' : 'All packages', + 'tooltip' : 'All packages that have been built', + 'filter' : {}, + 'search' : 'Search packages by name', + 'searchtip' : 'Enter a package name to find it', + 'columns' : [{ + 'col_name' : 'Package name', + 'col_id' : PackageListModel.COL_NAME, + 'col_style': 'text', + 'col_min' : 100, + 'col_max' : 400, + 'expand' : 'True' + }, { + 'col_name' : 'Size', + 'col_id' : PackageListModel.COL_SIZE, + 'col_style': 'text', + 'col_min' : 100, + 'col_max' : 500, + 'expand' : 'True' + }, { + 'col_name' : 'Recipe', + 'col_id' : PackageListModel.COL_RCP, + 'col_style': 'text', + 'col_min' : 100, + 'col_max' : 250, + 'expand' : 'True' + }, { + 'col_name' : 'Included', + 'col_id' : PackageListModel.COL_INC, + 'col_style': 'check toggle', + 'col_min' : 100, + 'col_max' : 100 + }] + } + ] + + (INCLUDED, + ALL) = range(2) + + def __init__(self, builder): + super(PackageSelectionPage, self).__init__(builder, "Edit packages") + + # set invisible members + self.recipe_model = self.builder.recipe_model + self.package_model = self.builder.package_model + + # create visual elements + self.create_visual_elements() + + def included_clicked_cb(self, button): + self.ins.set_current_page(self.INCLUDED) + + def create_visual_elements(self): + self.label = gtk.Label("Packages included: 0\nSelected packages size: 0 MB") + self.eventbox = self.add_onto_top_bar(self.label, 73) + self.pack_start(self.eventbox, expand=False, fill=False) + self.pack_start(self.group_align, expand=True, fill=True) + + # set visible members + self.ins = HobNotebook() + self.tables = [] # we need to modify table when the dialog is shown + + search_names = [] + search_tips = [] + # append the tab + for page in self.pages: + columns = page['columns'] + name = page['name'] + tab = HobViewTable(columns, name) + search_names.append(page['search']) + search_tips.append(page['searchtip']) + filter = page['filter'] + sort_model = self.package_model.tree_model(filter, initial=True) + tab.set_model(sort_model) + tab.connect("toggled", self.table_toggled_cb, name) + tab.connect("button-release-event", self.button_click_cb) + tab.connect("cell-fadeinout-stopped", self.after_fadeout_checkin_include, filter) + self.ins.append_page(tab, page['name'], page['tooltip']) + self.tables.append(tab) + + self.ins.set_entry(search_names, search_tips) + self.ins.search.connect("changed", self.search_entry_changed) + + # add all into the dialog + self.box_group_area.pack_start(self.ins, expand=True, fill=True) + + self.button_box = gtk.HBox(False, 6) + self.box_group_area.pack_start(self.button_box, expand=False, fill=False) + + self.build_image_button = HobButton('Build image') + #self.build_image_button.set_size_request(205, 49) + self.build_image_button.set_tooltip_text("Build target image") + self.build_image_button.set_flags(gtk.CAN_DEFAULT) + self.build_image_button.grab_default() + self.build_image_button.connect("clicked", self.build_image_clicked_cb) + self.button_box.pack_end(self.build_image_button, expand=False, fill=False) + + self.back_button = HobAltButton('Cancel') + self.back_button.connect("clicked", self.back_button_clicked_cb) + self.button_box.pack_end(self.back_button, expand=False, fill=False) + + def search_entry_changed(self, entry): + text = entry.get_text() + if self.ins.search_focus: + self.ins.search_focus = False + elif self.ins.page_changed: + self.ins.page_change = False + self.filter_search(entry) + elif text not in self.ins.search_names: + self.filter_search(entry) + + def filter_search(self, entry): + text = entry.get_text() + current_tab = self.ins.get_current_page() + filter = self.pages[current_tab]['filter'] + filter[PackageListModel.COL_NAME] = text + self.tables[current_tab].set_model(self.package_model.tree_model(filter, search_data=text)) + if self.package_model.filtered_nb == 0: + if not self.ins.get_nth_page(current_tab).top_bar: + self.ins.get_nth_page(current_tab).add_no_result_bar(entry) + self.ins.get_nth_page(current_tab).top_bar.set_no_show_all(True) + self.ins.get_nth_page(current_tab).top_bar.show() + self.ins.get_nth_page(current_tab).scroll.hide() + else: + if self.ins.get_nth_page(current_tab).top_bar: + self.ins.get_nth_page(current_tab).top_bar.hide() + self.ins.get_nth_page(current_tab).scroll.show() + if entry.get_text() == '': + entry.set_icon_sensitive(gtk.ENTRY_ICON_SECONDARY, False) + else: + entry.set_icon_sensitive(gtk.ENTRY_ICON_SECONDARY, True) + + def button_click_cb(self, widget, event): + path, col = widget.table_tree.get_cursor() + tree_model = widget.table_tree.get_model() + if path and col.get_title() != 'Included': # else activation is likely a removal + properties = {'binb': '' , 'name': '', 'size':'', 'recipe':'', 'files_list':''} + properties['binb'] = tree_model.get_value(tree_model.get_iter(path), PackageListModel.COL_BINB) + properties['name'] = tree_model.get_value(tree_model.get_iter(path), PackageListModel.COL_NAME) + properties['size'] = tree_model.get_value(tree_model.get_iter(path), PackageListModel.COL_SIZE) + properties['recipe'] = tree_model.get_value(tree_model.get_iter(path), PackageListModel.COL_RCP) + properties['files_list'] = tree_model.get_value(tree_model.get_iter(path), PackageListModel.COL_FLIST) + + self.builder.show_recipe_property_dialog(properties) + + def open_log_clicked_cb(self, button, log_file): + if log_file: + log_file = "file:///" + log_file + gtk.show_uri(screen=button.get_screen(), uri=log_file, timestamp=0) + + def show_page(self, log_file): + children = self.button_box.get_children() or [] + for child in children: + self.button_box.remove(child) + # re-packed the buttons as request, add the 'open log' button if build success + self.button_box.pack_end(self.build_image_button, expand=False, fill=False) + if log_file: + open_log_button = HobAltButton("Open log") + open_log_button.connect("clicked", self.open_log_clicked_cb, log_file) + open_log_button.set_tooltip_text("Open the build's log file") + self.button_box.pack_end(open_log_button, expand=False, fill=False) + self.button_box.pack_end(self.back_button, expand=False, fill=False) + self.show_all() + + def build_image_clicked_cb(self, button): + self.builder.parsing_warnings = [] + self.builder.build_image() + + def refresh_tables(self): + self.ins.reset_entry(self.ins.search, 0) + for tab in self.tables: + index = self.tables.index(tab) + filter = self.pages[index]['filter'] + tab.set_model(self.package_model.tree_model(filter, initial=True)) + + def back_button_clicked_cb(self, button): + if self.builder.previous_step == self.builder.IMAGE_GENERATED: + self.builder.restore_initial_selected_packages() + self.refresh_selection() + self.builder.show_image_details() + else: + self.builder.show_configuration() + self.refresh_tables() + + def refresh_selection(self): + self.builder.configuration.selected_packages = self.package_model.get_selected_packages() + self.builder.configuration.user_selected_packages = self.package_model.get_user_selected_packages() + selected_packages_num = len(self.builder.configuration.selected_packages) + selected_packages_size = self.package_model.get_packages_size() + selected_packages_size_str = HobPage._size_to_string(selected_packages_size) + + if self.builder.configuration.image_packages == self.builder.configuration.selected_packages: + image_total_size_str = self.builder.configuration.image_size + else: + image_overhead_factor = self.builder.configuration.image_overhead_factor + image_rootfs_size = self.builder.configuration.image_rootfs_size / 1024 # image_rootfs_size is KB + image_extra_size = self.builder.configuration.image_extra_size / 1024 # image_extra_size is KB + base_size = image_overhead_factor * selected_packages_size + image_total_size = max(base_size, image_rootfs_size) + image_extra_size + if "zypper" in self.builder.configuration.selected_packages: + image_total_size += (51200 * 1024) + image_total_size_str = HobPage._size_to_string(image_total_size) + + self.label.set_label("Packages included: %s\nSelected packages size: %s\nEstimated image size: %s" % + (selected_packages_num, selected_packages_size_str, image_total_size_str)) + self.ins.show_indicator_icon("Included packages", selected_packages_num) + + def toggle_item_idle_cb(self, path, view_tree, cell, pagename): + if not self.package_model.path_included(path): + self.package_model.include_item(item_path=path, binb="User Selected") + else: + self.pre_fadeout_checkout_include(view_tree) + self.package_model.exclude_item(item_path=path) + self.render_fadeout(view_tree, cell) + + self.refresh_selection() + if not self.builder.customized: + self.builder.customized = True + self.builder.set_base_image() + self.builder.configuration.selected_image = self.recipe_model.__custom_image__ + self.builder.rcppkglist_populated() + + self.builder.window_sensitive(True) + view_model = view_tree.get_model() + vpath = self.package_model.convert_path_to_vpath(view_model, path) + view_tree.set_cursor(vpath) + + def table_toggled_cb(self, table, cell, view_path, toggled_columnid, view_tree, pagename): + # Click to include a package + self.builder.window_sensitive(False) + view_model = view_tree.get_model() + path = self.package_model.convert_vpath_to_path(view_model, view_path) + glib.idle_add(self.toggle_item_idle_cb, path, view_tree, cell, pagename) + + def pre_fadeout_checkout_include(self, tree): + #after the fadeout the table will be sorted as before + self.sort_column_id = self.package_model.sort_column_id + self.sort_order = self.package_model.sort_order + + self.package_model.resync_fadeout_column(self.package_model.get_iter_first()) + # Check out a model which base on the column COL_FADE_INC, + # it's save the prev state of column COL_INC before do exclude_item + filter = { PackageListModel.COL_FADE_INC : [True]} + new_model = self.package_model.tree_model(filter, excluded_items_ahead=True) + tree.set_model(new_model) + tree.expand_all() + + def get_excluded_rows(self, to_render_cells, model, it): + while it: + path = model.get_path(it) + prev_cell_is_active = model.get_value(it, PackageListModel.COL_FADE_INC) + curr_cell_is_active = model.get_value(it, PackageListModel.COL_INC) + if (prev_cell_is_active == True) and (curr_cell_is_active == False): + to_render_cells.append(path) + if model.iter_has_child(it): + self.get_excluded_rows(to_render_cells, model, model.iter_children(it)) + it = model.iter_next(it) + + return to_render_cells + + def render_fadeout(self, tree, cell): + if (not cell) or (not tree): + return + to_render_cells = [] + view_model = tree.get_model() + self.get_excluded_rows(to_render_cells, view_model, view_model.get_iter_first()) + + cell.fadeout(tree, 1000, to_render_cells) + + def after_fadeout_checkin_include(self, table, ctrl, cell, tree, filter): + self.package_model.sort_column_id = self.sort_column_id + self.package_model.sort_order = self.sort_order + tree.set_model(self.package_model.tree_model(filter)) + tree.expand_all() + + def set_packages_curr_tab(self, curr_page): + self.ins.set_current_page(curr_page) + diff --git a/bitbake/lib/bb/ui/crumbs/persistenttooltip.py b/bitbake/lib/bb/ui/crumbs/persistenttooltip.py new file mode 100644 index 0000000000..927c194292 --- /dev/null +++ b/bitbake/lib/bb/ui/crumbs/persistenttooltip.py @@ -0,0 +1,186 @@ +# +# BitBake Graphical GTK User Interface +# +# Copyright (C) 2012 Intel Corporation +# +# Authored by Joshua Lock +# +# 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 gobject +import gtk +try: + import gconf +except: + pass + +class PersistentTooltip(gtk.Window): + """ + A tooltip which persists once shown until the user dismisses it with the Esc + key or by clicking the close button. + + # FIXME: the PersistentTooltip should be disabled when the user clicks anywhere off + # it. We can't do this with focus-out-event becuase modal ensures we have focus? + + markup: some Pango text markup to display in the tooltip + """ + def __init__(self, markup, parent_win=None): + gtk.Window.__init__(self, gtk.WINDOW_POPUP) + + # Inherit the system theme for a tooltip + style = gtk.rc_get_style_by_paths(gtk.settings_get_default(), + 'gtk-tooltip', 'gtk-tooltip', gobject.TYPE_NONE) + self.set_style(style) + + # The placement of the close button on the tip should reflect how the + # window manager of the users system places close buttons. Try to read + # the metacity gconf key to determine whether the close button is on the + # left or the right. + # In the case that we can't determine the users configuration we default + # to close buttons being on the right. + __button_right = True + try: + client = gconf.client_get_default() + order = client.get_string("/apps/metacity/general/button_layout") + if order and order.endswith(":"): + __button_right = False + except NameError: + pass + + # We need to ensure we're only shown once + self.shown = False + + # We don't want any WM decorations + self.set_decorated(False) + # We don't want to show in the taskbar or window switcher + self.set_skip_pager_hint(True) + self.set_skip_taskbar_hint(True) + # We must be modal to ensure we grab focus when presented from a gtk.Dialog + self.set_modal(True) + + self.set_border_width(0) + self.set_position(gtk.WIN_POS_MOUSE) + self.set_opacity(0.95) + + # Ensure a reasonable minimum size + self.set_geometry_hints(self, 100, 50) + + # Set this window as a transient window for parent(main window) + if parent_win: + self.set_transient_for(parent_win) + self.set_destroy_with_parent(True) + # Draw our label and close buttons + hbox = gtk.HBox(False, 0) + hbox.show() + self.add(hbox) + + img = gtk.Image() + img.set_from_stock(gtk.STOCK_CLOSE, gtk.ICON_SIZE_BUTTON) + + self.button = gtk.Button() + self.button.set_image(img) + self.button.connect("clicked", self._dismiss_cb) + self.button.set_flags(gtk.CAN_DEFAULT) + self.button.grab_focus() + self.button.show() + vbox = gtk.VBox(False, 0) + vbox.show() + vbox.pack_start(self.button, False, False, 0) + if __button_right: + hbox.pack_end(vbox, True, True, 0) + else: + hbox.pack_start(vbox, True, True, 0) + + self.set_default(self.button) + + bin = gtk.HBox(True, 6) + bin.set_border_width(6) + bin.show() + self.label = gtk.Label() + self.label.set_line_wrap(True) + # We want to match the colours of the normal tooltips, as dictated by + # the users gtk+-2.0 theme, wherever possible - on some systems this + # requires explicitly setting a fg_color for the label which matches the + # tooltip_fg_color + settings = gtk.settings_get_default() + colours = settings.get_property('gtk-color-scheme').split('\n') + # remove any empty lines, there's likely to be a trailing one after + # calling split on a dictionary-like string + colours = filter(None, colours) + for col in colours: + item, val = col.split(': ') + if item == 'tooltip_fg_color': + style = self.label.get_style() + style.fg[gtk.STATE_NORMAL] = gtk.gdk.color_parse(val) + self.label.set_style(style) + break # we only care for the tooltip_fg_color + + self.label.set_markup(markup) + self.label.show() + bin.add(self.label) + hbox.pack_end(bin, True, True, 6) + + # add the original URL display for user reference + if 'a href' in markup: + hbox.set_tooltip_text(self.get_markup_url(markup)) + hbox.show() + + self.connect("key-press-event", self._catch_esc_cb) + + """ + Callback when the PersistentTooltip's close button is clicked. + Hides the PersistentTooltip. + """ + def _dismiss_cb(self, button): + self.hide() + return True + + """ + Callback when the Esc key is detected. Hides the PersistentTooltip. + """ + def _catch_esc_cb(self, widget, event): + keyname = gtk.gdk.keyval_name(event.keyval) + if keyname == "Escape": + self.hide() + return True + + """ + Called to present the PersistentTooltip. + Overrides the superclasses show() method to include state tracking. + """ + def show(self): + if not self.shown: + self.shown = True + gtk.Window.show(self) + + """ + Called to hide the PersistentTooltip. + Overrides the superclasses hide() method to include state tracking. + """ + def hide(self): + self.shown = False + gtk.Window.hide(self) + + """ + Called to get the hyperlink URL from markup text. + """ + def get_markup_url(self, markup): + url = "http:" + if markup and type(markup) == str: + s = markup + if 'http:' in s: + import re + url = re.search('(http:[^,\\ "]+)', s).group(0) + + return url diff --git a/bitbake/lib/bb/ui/crumbs/progress.py b/bitbake/lib/bb/ui/crumbs/progress.py new file mode 100644 index 0000000000..1d28a111b3 --- /dev/null +++ b/bitbake/lib/bb/ui/crumbs/progress.py @@ -0,0 +1,23 @@ +import gtk + +class ProgressBar(gtk.Dialog): + def __init__(self, parent): + + gtk.Dialog.__init__(self, flags=(gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT)) + self.set_title("Parsing metadata, please wait...") + self.set_default_size(500, 0) + self.set_transient_for(parent) + self.progress = gtk.ProgressBar() + self.vbox.pack_start(self.progress) + self.show_all() + + def set_text(self, msg): + self.progress.set_text(msg) + + def update(self, x, y): + self.progress.set_fraction(float(x)/float(y)) + self.progress.set_text("%2d %%" % (x*100/y)) + + def pulse(self): + self.progress.set_text("Loading...") + self.progress.pulse() diff --git a/bitbake/lib/bb/ui/crumbs/progressbar.py b/bitbake/lib/bb/ui/crumbs/progressbar.py new file mode 100644 index 0000000000..3e2c660e4a --- /dev/null +++ b/bitbake/lib/bb/ui/crumbs/progressbar.py @@ -0,0 +1,59 @@ +# BitBake Graphical GTK User Interface +# +# Copyright (C) 2011 Intel Corporation +# +# Authored by Shane Wang +# +# 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 +from bb.ui.crumbs.hobcolor import HobColors + +class HobProgressBar (gtk.ProgressBar): + def __init__(self): + gtk.ProgressBar.__init__(self) + self.set_rcstyle(True) + self.percentage = 0 + + def set_rcstyle(self, status): + rcstyle = gtk.RcStyle() + rcstyle.fg[2] = gtk.gdk.Color(HobColors.BLACK) + if status == "stop": + rcstyle.bg[3] = gtk.gdk.Color(HobColors.WARNING) + elif status == "fail": + rcstyle.bg[3] = gtk.gdk.Color(HobColors.ERROR) + else: + rcstyle.bg[3] = gtk.gdk.Color(HobColors.RUNNING) + self.modify_style(rcstyle) + + def set_title(self, text=None): + if not text: + text = "" + text += " %.0f%%" % self.percentage + self.set_text(text) + + def set_stop_title(self, text=None): + if not text: + text = "" + self.set_text(text) + + def reset(self): + self.set_fraction(0) + self.set_text("") + self.set_rcstyle(True) + self.percentage = 0 + + def update(self, fraction): + self.percentage = int(fraction * 100) + self.set_fraction(fraction) diff --git a/bitbake/lib/bb/ui/crumbs/puccho.glade b/bitbake/lib/bb/ui/crumbs/puccho.glade new file mode 100644 index 0000000000..d7553a6e14 --- /dev/null +++ b/bitbake/lib/bb/ui/crumbs/puccho.glade @@ -0,0 +1,606 @@ + + + + + + Start a build + GTK_WIN_POS_CENTER_ON_PARENT + GDK_WINDOW_TYPE_HINT_DIALOG + False + + + True + 2 + + + True + 6 + 7 + 3 + 5 + 6 + + + True + 12 + + + 6 + + + True + True + 0 + gtk-dialog-error + + + False + False + + + + + True + 0 + If you see this text something is wrong... + True + True + + + 1 + + + + + + + 3 + 2 + 3 + + + + + True + 0 + <b>Build configuration</b> + True + + + 3 + 3 + 4 + + + + + + True + False + + + 1 + 2 + 6 + 7 + + + + + + True + False + 0 + 12 + Image: + + + 6 + 7 + + + + + + True + False + + + 1 + 2 + 5 + 6 + + + + + + True + False + 0 + 12 + Distribution: + + + 5 + 6 + + + + + + True + False + + + 1 + 2 + 4 + 5 + + + + + + True + False + 0 + 12 + Machine: + + + 4 + 5 + + + + + + True + False + True + True + gtk-refresh + True + 0 + + + 2 + 3 + 1 + 2 + + + + + + True + True + 32 + + + 1 + 2 + 1 + 2 + + + + + + True + 0 + 12 + Location: + + + 1 + 2 + + + + + + True + 0 + <b>Repository</b> + True + + + 3 + + + + + + True + + + + + + 2 + 3 + 4 + 5 + + + + + + True + + + + + + 2 + 3 + 5 + 6 + + + + + + True + + + + + + 2 + 3 + 6 + 7 + + + + + + 1 + + + + + True + GTK_BUTTONBOX_END + + + + + + + + + + + + False + GTK_PACK_END + + + + + + + GTK_WIN_POS_CENTER_ON_PARENT + GDK_WINDOW_TYPE_HINT_DIALOG + False + + + True + 2 + + + True + 6 + 7 + 3 + 6 + 6 + + + True + 0 + <b>Repositories</b> + True + + + 3 + + + + + + True + 0 + 12 + + + True + True + GTK_POLICY_AUTOMATIC + GTK_POLICY_AUTOMATIC + + + True + True + True + + + + + + + 3 + 2 + 3 + + + + + + True + True + + + 1 + 3 + 1 + 2 + + + + + + True + 0 + <b>Additional packages</b> + True + + + 3 + 4 + 5 + + + + + + True + 0 + 0 + + + True + 0 + 0 + 12 + Location: + + + + + 1 + 2 + + + + + + True + 1 + 0 + + + True + 5 + + + True + True + True + gtk-remove + True + 0 + + + + + True + True + True + gtk-edit + True + 0 + + + 1 + + + + + True + True + True + gtk-add + True + 0 + + + 2 + + + + + + + 1 + 3 + 3 + 4 + + + + + + True + + + + + + 3 + 4 + + + + + + True + 0 + 0 + 12 + Search: + + + 5 + 6 + + + + + + True + True + + + 1 + 3 + 5 + 6 + + + + + + True + 0 + 12 + + + True + True + GTK_POLICY_AUTOMATIC + GTK_POLICY_AUTOMATIC + + + True + True + True + + + + + + + 3 + 6 + 7 + + + + + + 1 + + + + + True + GTK_BUTTONBOX_END + + + True + True + True + gtk-close + True + 0 + + + + + False + GTK_PACK_END + + + + + + + + + True + + + True + + + True + Build + gtk-execute + + + False + + + + + False + + + + + True + True + + + True + True + GTK_POLICY_AUTOMATIC + GTK_POLICY_AUTOMATIC + + + + + + False + True + + + + + True + True + GTK_POLICY_AUTOMATIC + GTK_POLICY_AUTOMATIC + + + + + + True + True + + + + + 1 + + + + + + diff --git a/bitbake/lib/bb/ui/crumbs/recipeselectionpage.py b/bitbake/lib/bb/ui/crumbs/recipeselectionpage.py new file mode 100755 index 0000000000..58db43f706 --- /dev/null +++ b/bitbake/lib/bb/ui/crumbs/recipeselectionpage.py @@ -0,0 +1,335 @@ +#!/usr/bin/env python +# +# BitBake Graphical GTK User Interface +# +# Copyright (C) 2012 Intel Corporation +# +# Authored by Dongxiao Xu +# Authored by Shane Wang +# +# 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 glib +from bb.ui.crumbs.hobcolor import HobColors +from bb.ui.crumbs.hobwidget import HobViewTable, HobNotebook, HobAltButton, HobButton +from bb.ui.crumbs.hoblistmodel import RecipeListModel +from bb.ui.crumbs.hobpages import HobPage + +# +# RecipeSelectionPage +# +class RecipeSelectionPage (HobPage): + pages = [ + { + 'name' : 'Included recipes', + 'tooltip' : 'The recipes currently included for your image', + 'filter' : { RecipeListModel.COL_INC : [True], + RecipeListModel.COL_TYPE : ['recipe', 'packagegroup'] }, + 'search' : 'Search recipes by name', + 'searchtip' : 'Enter a recipe name to find it', + 'columns' : [{ + 'col_name' : 'Recipe name', + 'col_id' : RecipeListModel.COL_NAME, + 'col_style': 'text', + 'col_min' : 100, + 'col_max' : 400, + 'expand' : 'True' + }, { + 'col_name' : 'Group', + 'col_id' : RecipeListModel.COL_GROUP, + 'col_style': 'text', + 'col_min' : 100, + 'col_max' : 300, + 'expand' : 'True' + }, { + 'col_name' : 'Brought in by (+others)', + 'col_id' : RecipeListModel.COL_BINB, + 'col_style': 'binb', + 'col_min' : 100, + 'col_max' : 500, + 'expand' : 'True' + }, { + 'col_name' : 'Included', + 'col_id' : RecipeListModel.COL_INC, + 'col_style': 'check toggle', + 'col_min' : 100, + 'col_max' : 100 + }] + }, { + 'name' : 'All recipes', + 'tooltip' : 'All recipes in your configured layers', + 'filter' : { RecipeListModel.COL_TYPE : ['recipe'] }, + 'search' : 'Search recipes by name', + 'searchtip' : 'Enter a recipe name to find it', + 'columns' : [{ + 'col_name' : 'Recipe name', + 'col_id' : RecipeListModel.COL_NAME, + 'col_style': 'text', + 'col_min' : 100, + 'col_max' : 400, + 'expand' : 'True' + }, { + 'col_name' : 'Group', + 'col_id' : RecipeListModel.COL_GROUP, + 'col_style': 'text', + 'col_min' : 100, + 'col_max' : 400, + 'expand' : 'True' + }, { + 'col_name' : 'License', + 'col_id' : RecipeListModel.COL_LIC, + 'col_style': 'text', + 'col_min' : 100, + 'col_max' : 400, + 'expand' : 'True' + }, { + 'col_name' : 'Included', + 'col_id' : RecipeListModel.COL_INC, + 'col_style': 'check toggle', + 'col_min' : 100, + 'col_max' : 100 + }] + }, { + 'name' : 'Package Groups', + 'tooltip' : 'All package groups in your configured layers', + 'filter' : { RecipeListModel.COL_TYPE : ['packagegroup'] }, + 'search' : 'Search package groups by name', + 'searchtip' : 'Enter a package group name to find it', + 'columns' : [{ + 'col_name' : 'Package group name', + 'col_id' : RecipeListModel.COL_NAME, + 'col_style': 'text', + 'col_min' : 100, + 'col_max' : 400, + 'expand' : 'True' + }, { + 'col_name' : 'Included', + 'col_id' : RecipeListModel.COL_INC, + 'col_style': 'check toggle', + 'col_min' : 100, + 'col_max' : 100 + }] + } + ] + + (INCLUDED, + ALL, + TASKS) = range(3) + + def __init__(self, builder = None): + super(RecipeSelectionPage, self).__init__(builder, "Step 1 of 2: Edit recipes") + + # set invisible members + self.recipe_model = self.builder.recipe_model + + # create visual elements + self.create_visual_elements() + + def included_clicked_cb(self, button): + self.ins.set_current_page(self.INCLUDED) + + def create_visual_elements(self): + self.eventbox = self.add_onto_top_bar(None, 73) + self.pack_start(self.eventbox, expand=False, fill=False) + self.pack_start(self.group_align, expand=True, fill=True) + + # set visible members + self.ins = HobNotebook() + self.tables = [] # we need modify table when the dialog is shown + + search_names = [] + search_tips = [] + # append the tabs in order + for page in self.pages: + columns = page['columns'] + name = page['name'] + tab = HobViewTable(columns, name) + search_names.append(page['search']) + search_tips.append(page['searchtip']) + filter = page['filter'] + sort_model = self.recipe_model.tree_model(filter, initial=True) + tab.set_model(sort_model) + tab.connect("toggled", self.table_toggled_cb, name) + tab.connect("button-release-event", self.button_click_cb) + tab.connect("cell-fadeinout-stopped", self.after_fadeout_checkin_include, filter) + self.ins.append_page(tab, page['name'], page['tooltip']) + self.tables.append(tab) + + self.ins.set_entry(search_names, search_tips) + self.ins.search.connect("changed", self.search_entry_changed) + + # add all into the window + self.box_group_area.pack_start(self.ins, expand=True, fill=True) + + button_box = gtk.HBox(False, 6) + self.box_group_area.pack_end(button_box, expand=False, fill=False) + + self.build_packages_button = HobButton('Build packages') + #self.build_packages_button.set_size_request(205, 49) + self.build_packages_button.set_tooltip_text("Build selected recipes into packages") + self.build_packages_button.set_flags(gtk.CAN_DEFAULT) + self.build_packages_button.grab_default() + self.build_packages_button.connect("clicked", self.build_packages_clicked_cb) + button_box.pack_end(self.build_packages_button, expand=False, fill=False) + + self.back_button = HobAltButton('Cancel') + self.back_button.connect("clicked", self.back_button_clicked_cb) + button_box.pack_end(self.back_button, expand=False, fill=False) + + def search_entry_changed(self, entry): + text = entry.get_text() + if self.ins.search_focus: + self.ins.search_focus = False + elif self.ins.page_changed: + self.ins.page_change = False + self.filter_search(entry) + elif text not in self.ins.search_names: + self.filter_search(entry) + + def filter_search(self, entry): + text = entry.get_text() + current_tab = self.ins.get_current_page() + filter = self.pages[current_tab]['filter'] + filter[RecipeListModel.COL_NAME] = text + self.tables[current_tab].set_model(self.recipe_model.tree_model(filter, search_data=text)) + if self.recipe_model.filtered_nb == 0: + if not self.ins.get_nth_page(current_tab).top_bar: + self.ins.get_nth_page(current_tab).add_no_result_bar(entry) + self.ins.get_nth_page(current_tab).top_bar.set_no_show_all(True) + self.ins.get_nth_page(current_tab).top_bar.show() + self.ins.get_nth_page(current_tab).scroll.hide() + else: + if self.ins.get_nth_page(current_tab).top_bar: + self.ins.get_nth_page(current_tab).top_bar.hide() + self.ins.get_nth_page(current_tab).scroll.show() + if entry.get_text() == '': + entry.set_icon_sensitive(gtk.ENTRY_ICON_SECONDARY, False) + else: + entry.set_icon_sensitive(gtk.ENTRY_ICON_SECONDARY, True) + + def button_click_cb(self, widget, event): + path, col = widget.table_tree.get_cursor() + tree_model = widget.table_tree.get_model() + if path and col.get_title() != 'Included': # else activation is likely a removal + properties = {'summary': '', 'name': '', 'version': '', 'revision': '', 'binb': '', 'group': '', 'license': '', 'homepage': '', 'bugtracker': '', 'description': ''} + properties['summary'] = tree_model.get_value(tree_model.get_iter(path), RecipeListModel.COL_SUMMARY) + properties['name'] = tree_model.get_value(tree_model.get_iter(path), RecipeListModel.COL_NAME) + properties['version'] = tree_model.get_value(tree_model.get_iter(path), RecipeListModel.COL_VERSION) + properties['revision'] = tree_model.get_value(tree_model.get_iter(path), RecipeListModel.COL_REVISION) + properties['binb'] = tree_model.get_value(tree_model.get_iter(path), RecipeListModel.COL_BINB) + properties['group'] = tree_model.get_value(tree_model.get_iter(path), RecipeListModel.COL_GROUP) + properties['license'] = tree_model.get_value(tree_model.get_iter(path), RecipeListModel.COL_LIC) + properties['homepage'] = tree_model.get_value(tree_model.get_iter(path), RecipeListModel.COL_HOMEPAGE) + properties['bugtracker'] = tree_model.get_value(tree_model.get_iter(path), RecipeListModel.COL_BUGTRACKER) + properties['description'] = tree_model.get_value(tree_model.get_iter(path), RecipeListModel.COL_DESC) + self.builder.show_recipe_property_dialog(properties) + + def build_packages_clicked_cb(self, button): + self.refresh_tables() + self.builder.build_packages() + + def refresh_tables(self): + self.ins.reset_entry(self.ins.search, 0) + for tab in self.tables: + index = self.tables.index(tab) + filter = self.pages[index]['filter'] + tab.set_model(self.recipe_model.tree_model(filter, search_data="", initial=True)) + + def back_button_clicked_cb(self, button): + self.builder.recipe_model.set_selected_image(self.builder.configuration.initial_selected_image) + self.builder.image_configuration_page.update_image_combo(self.builder.recipe_model, self.builder.configuration.initial_selected_image) + self.builder.image_configuration_page.update_image_desc() + self.builder.show_configuration() + self.refresh_tables() + + def refresh_selection(self): + self.builder.configuration.selected_image = self.recipe_model.get_selected_image() + _, self.builder.configuration.selected_recipes = self.recipe_model.get_selected_recipes() + self.ins.show_indicator_icon("Included recipes", len(self.builder.configuration.selected_recipes)) + + def toggle_item_idle_cb(self, path, view_tree, cell, pagename): + if not self.recipe_model.path_included(path): + self.recipe_model.include_item(item_path=path, binb="User Selected", image_contents=False) + else: + self.pre_fadeout_checkout_include(view_tree, pagename) + self.recipe_model.exclude_item(item_path=path) + self.render_fadeout(view_tree, cell) + + self.refresh_selection() + if not self.builder.customized: + self.builder.customized = True + self.builder.configuration.selected_image = self.recipe_model.__custom_image__ + self.builder.rcppkglist_populated() + + self.builder.window_sensitive(True) + + view_model = view_tree.get_model() + vpath = self.recipe_model.convert_path_to_vpath(view_model, path) + view_tree.set_cursor(vpath) + + def table_toggled_cb(self, table, cell, view_path, toggled_columnid, view_tree, pagename): + # Click to include a recipe + self.builder.window_sensitive(False) + view_model = view_tree.get_model() + path = self.recipe_model.convert_vpath_to_path(view_model, view_path) + glib.idle_add(self.toggle_item_idle_cb, path, view_tree, cell, pagename) + + def pre_fadeout_checkout_include(self, tree, pagename): + #after the fadeout the table will be sorted as before + self.sort_column_id = self.recipe_model.sort_column_id + self.sort_order = self.recipe_model.sort_order + + #resync the included items to a backup fade include column + it = self.recipe_model.get_iter_first() + while it: + active = self.recipe_model.get_value(it, self.recipe_model.COL_INC) + self.recipe_model.set(it, self.recipe_model.COL_FADE_INC, active) + it = self.recipe_model.iter_next(it) + # Check out a model which base on the column COL_FADE_INC, + # it's save the prev state of column COL_INC before do exclude_item + filter = { RecipeListModel.COL_FADE_INC:[True] } + if pagename == "Included recipes": + filter[RecipeListModel.COL_TYPE] = ['recipe', 'packagegroup'] + elif pagename == "All recipes": + filter[RecipeListModel.COL_TYPE] = ['recipe'] + else: + filter[RecipeListModel.COL_TYPE] = ['packagegroup'] + + new_model = self.recipe_model.tree_model(filter, excluded_items_ahead=True) + tree.set_model(new_model) + + def render_fadeout(self, tree, cell): + if (not cell) or (not tree): + return + to_render_cells = [] + model = tree.get_model() + it = model.get_iter_first() + while it: + path = model.get_path(it) + prev_cell_is_active = model.get_value(it, RecipeListModel.COL_FADE_INC) + curr_cell_is_active = model.get_value(it, RecipeListModel.COL_INC) + if (prev_cell_is_active == True) and (curr_cell_is_active == False): + to_render_cells.append(path) + it = model.iter_next(it) + + cell.fadeout(tree, 1000, to_render_cells) + + def after_fadeout_checkin_include(self, table, ctrl, cell, tree, filter): + self.recipe_model.sort_column_id = self.sort_column_id + self.recipe_model.sort_order = self.sort_order + tree.set_model(self.recipe_model.tree_model(filter)) + + def set_recipe_curr_tab(self, curr_page): + self.ins.set_current_page(curr_page) diff --git a/bitbake/lib/bb/ui/crumbs/runningbuild.py b/bitbake/lib/bb/ui/crumbs/runningbuild.py new file mode 100644 index 0000000000..16a955d2b1 --- /dev/null +++ b/bitbake/lib/bb/ui/crumbs/runningbuild.py @@ -0,0 +1,551 @@ + +# +# BitBake Graphical GTK User Interface +# +# Copyright (C) 2008 Intel Corporation +# +# Authored by Rob Bradford +# +# 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 logging +import time +import urllib +import urllib2 +import pango +from bb.ui.crumbs.hobcolor import HobColors +from bb.ui.crumbs.hobwidget import HobWarpCellRendererText, HobCellRendererPixbuf + +class RunningBuildModel (gtk.TreeStore): + (COL_LOG, COL_PACKAGE, COL_TASK, COL_MESSAGE, COL_ICON, COL_COLOR, COL_NUM_ACTIVE) = range(7) + + def __init__ (self): + gtk.TreeStore.__init__ (self, + gobject.TYPE_STRING, + gobject.TYPE_STRING, + gobject.TYPE_STRING, + gobject.TYPE_STRING, + gobject.TYPE_STRING, + gobject.TYPE_STRING, + gobject.TYPE_INT) + + def failure_model_filter(self, model, it): + color = model.get(it, self.COL_COLOR)[0] + if not color: + return False + if color == HobColors.ERROR or color == HobColors.WARNING: + return True + return False + + def failure_model(self): + model = self.filter_new() + model.set_visible_func(self.failure_model_filter) + return model + + def foreach_cell_func(self, model, path, iter, usr_data=None): + if model.get_value(iter, self.COL_ICON) == "gtk-execute": + model.set(iter, self.COL_ICON, "") + + def close_task_refresh(self): + self.foreach(self.foreach_cell_func, None) + +class RunningBuild (gobject.GObject): + __gsignals__ = { + 'build-started' : (gobject.SIGNAL_RUN_LAST, + gobject.TYPE_NONE, + ()), + 'build-succeeded' : (gobject.SIGNAL_RUN_LAST, + gobject.TYPE_NONE, + ()), + 'build-failed' : (gobject.SIGNAL_RUN_LAST, + gobject.TYPE_NONE, + ()), + 'build-complete' : (gobject.SIGNAL_RUN_LAST, + gobject.TYPE_NONE, + ()), + 'build-aborted' : (gobject.SIGNAL_RUN_LAST, + gobject.TYPE_NONE, + ()), + 'task-started' : (gobject.SIGNAL_RUN_LAST, + gobject.TYPE_NONE, + (gobject.TYPE_PYOBJECT,)), + 'log-error' : (gobject.SIGNAL_RUN_LAST, + gobject.TYPE_NONE, + ()), + 'log-warning' : (gobject.SIGNAL_RUN_LAST, + gobject.TYPE_NONE, + ()), + 'disk-full' : (gobject.SIGNAL_RUN_LAST, + gobject.TYPE_NONE, + ()), + 'no-provider' : (gobject.SIGNAL_RUN_LAST, + gobject.TYPE_NONE, + (gobject.TYPE_PYOBJECT,)), + 'log' : (gobject.SIGNAL_RUN_LAST, + gobject.TYPE_NONE, + (gobject.TYPE_STRING, gobject.TYPE_PYOBJECT,)), + } + pids_to_task = {} + tasks_to_iter = {} + + def __init__ (self, sequential=False): + gobject.GObject.__init__ (self) + self.model = RunningBuildModel() + self.sequential = sequential + self.buildaborted = False + + def reset (self): + self.pids_to_task.clear() + self.tasks_to_iter.clear() + self.model.clear() + + def handle_event (self, event, pbar=None): + # Handle an event from the event queue, this may result in updating + # the model and thus the UI. Or it may be to tell us that the build + # has finished successfully (or not, as the case may be.) + + parent = None + pid = 0 + package = None + task = None + + # If we have a pid attached to this message/event try and get the + # (package, task) pair for it. If we get that then get the parent iter + # for the message. + if hasattr(event, 'pid'): + pid = event.pid + if hasattr(event, 'process'): + pid = event.process + + if pid and pid in self.pids_to_task: + (package, task) = self.pids_to_task[pid] + parent = self.tasks_to_iter[(package, task)] + + if(isinstance(event, logging.LogRecord)): + if event.taskpid == 0 or event.levelno > logging.INFO: + self.emit("log", "handle", event) + # FIXME: this is a hack! More info in Yocto #1433 + # http://bugzilla.pokylinux.org/show_bug.cgi?id=1433, temporarily + # mask the error message as it's not informative for the user. + if event.msg.startswith("Execution of event handler 'run_buildstats' failed"): + return + + if (event.levelno < logging.INFO or + event.msg.startswith("Running task")): + return # don't add these to the list + + if event.levelno >= logging.ERROR: + icon = "dialog-error" + color = HobColors.ERROR + self.emit("log-error") + elif event.levelno >= logging.WARNING: + icon = "dialog-warning" + color = HobColors.WARNING + self.emit("log-warning") + else: + icon = None + color = HobColors.OK + + # if we know which package we belong to, we'll append onto its list. + # otherwise, we'll jump to the top of the master list + if self.sequential or not parent: + tree_add = self.model.append + else: + tree_add = self.model.prepend + tree_add(parent, + (None, + package, + task, + event.getMessage(), + icon, + color, + 0)) + + # if there are warnings while processing a package + # (parent), mark the task with warning color; + # in case there are errors, the updates will be + # handled on TaskFailed. + if color == HobColors.WARNING and parent: + self.model.set(parent, self.model.COL_COLOR, color) + if task: #then we have a parent (package), and update it's color + self.model.set(self.tasks_to_iter[(package, None)], self.model.COL_COLOR, color) + + elif isinstance(event, bb.build.TaskStarted): + (package, task) = (event._package, event._task) + + # Save out this PID. + self.pids_to_task[pid] = (package, task) + + # Check if we already have this package in our model. If so then + # that can be the parent for the task. Otherwise we create a new + # top level for the package. + if ((package, None) in self.tasks_to_iter): + parent = self.tasks_to_iter[(package, None)] + else: + if self.sequential: + add = self.model.append + else: + add = self.model.prepend + parent = add(None, (None, + package, + None, + "Package: %s" % (package), + None, + HobColors.OK, + 0)) + self.tasks_to_iter[(package, None)] = parent + + # Because this parent package now has an active child mark it as + # such. + self.model.set(parent, self.model.COL_ICON, "gtk-execute") + parent_color = self.model.get(parent, self.model.COL_COLOR)[0] + if parent_color != HobColors.ERROR and parent_color != HobColors.WARNING: + self.model.set(parent, self.model.COL_COLOR, HobColors.RUNNING) + + # Add an entry in the model for this task + i = self.model.append (parent, (None, + package, + task, + "Task: %s" % (task), + "gtk-execute", + HobColors.RUNNING, + 0)) + + # update the parent's active task count + num_active = self.model.get(parent, self.model.COL_NUM_ACTIVE)[0] + 1 + self.model.set(parent, self.model.COL_NUM_ACTIVE, num_active) + + # Save out the iter so that we can find it when we have a message + # that we need to attach to a task. + self.tasks_to_iter[(package, task)] = i + + elif isinstance(event, bb.build.TaskBase): + self.emit("log", "info", event._message) + current = self.tasks_to_iter[(package, task)] + parent = self.tasks_to_iter[(package, None)] + + # remove this task from the parent's active count + num_active = self.model.get(parent, self.model.COL_NUM_ACTIVE)[0] - 1 + self.model.set(parent, self.model.COL_NUM_ACTIVE, num_active) + + if isinstance(event, bb.build.TaskFailed): + # Mark the task and parent as failed + icon = "dialog-error" + color = HobColors.ERROR + + logfile = event.logfile + if logfile and os.path.exists(logfile): + with open(logfile) as f: + logdata = f.read() + self.model.append(current, ('pastebin', None, None, logdata, 'gtk-error', HobColors.OK, 0)) + + for i in (current, parent): + self.model.set(i, self.model.COL_ICON, icon, + self.model.COL_COLOR, color) + else: + # Mark the parent package and the task as inactive, + # but make sure to preserve error, warnings and active + # states + parent_color = self.model.get(parent, self.model.COL_COLOR)[0] + task_color = self.model.get(current, self.model.COL_COLOR)[0] + + # Mark the task as inactive + self.model.set(current, self.model.COL_ICON, None) + if task_color != HobColors.ERROR: + if task_color == HobColors.WARNING: + self.model.set(current, self.model.COL_ICON, 'dialog-warning') + else: + self.model.set(current, self.model.COL_COLOR, HobColors.OK) + + # Mark the parent as inactive + if parent_color != HobColors.ERROR: + if parent_color == HobColors.WARNING: + self.model.set(parent, self.model.COL_ICON, "dialog-warning") + else: + self.model.set(parent, self.model.COL_ICON, None) + if num_active == 0: + self.model.set(parent, self.model.COL_COLOR, HobColors.OK) + + # Clear the iters and the pids since when the task goes away the + # pid will no longer be used for messages + del self.tasks_to_iter[(package, task)] + del self.pids_to_task[pid] + + elif isinstance(event, bb.event.BuildStarted): + + self.emit("build-started") + self.model.prepend(None, (None, + None, + None, + "Build Started (%s)" % time.strftime('%m/%d/%Y %H:%M:%S'), + None, + HobColors.OK, + 0)) + if pbar: + pbar.update(0, self.progress_total) + pbar.set_title(bb.event.getName(event)) + + elif isinstance(event, bb.event.BuildCompleted): + failures = int (event._failures) + self.model.prepend(None, (None, + None, + None, + "Build Completed (%s)" % time.strftime('%m/%d/%Y %H:%M:%S'), + None, + HobColors.OK, + 0)) + + # Emit the appropriate signal depending on the number of failures + if self.buildaborted: + self.emit ("build-aborted") + self.buildaborted = False + elif (failures >= 1): + self.emit ("build-failed") + else: + self.emit ("build-succeeded") + # Emit a generic "build-complete" signal for things wishing to + # handle when the build is finished + self.emit("build-complete") + # reset the all cell's icon indicator + self.model.close_task_refresh() + if pbar: + pbar.set_text(event.msg) + + elif isinstance(event, bb.event.DiskFull): + self.buildaborted = True + self.emit("disk-full") + + elif isinstance(event, bb.command.CommandFailed): + self.emit("log", "error", "Command execution failed: %s" % (event.error)) + if event.error.startswith("Exited with"): + # If the command fails with an exit code we're done, emit the + # generic signal for the UI to notify the user + self.emit("build-complete") + # reset the all cell's icon indicator + self.model.close_task_refresh() + + elif isinstance(event, bb.event.CacheLoadStarted) and pbar: + pbar.set_title("Loading cache") + self.progress_total = event.total + pbar.update(0, self.progress_total) + elif isinstance(event, bb.event.CacheLoadProgress) and pbar: + pbar.update(event.current, self.progress_total) + elif isinstance(event, bb.event.CacheLoadCompleted) and pbar: + pbar.update(self.progress_total, self.progress_total) + pbar.hide() + elif isinstance(event, bb.event.ParseStarted) and pbar: + if event.total == 0: + return + pbar.set_title("Processing recipes") + self.progress_total = event.total + pbar.update(0, self.progress_total) + elif isinstance(event, bb.event.ParseProgress) and pbar: + pbar.update(event.current, self.progress_total) + elif isinstance(event, bb.event.ParseCompleted) and pbar: + pbar.hide() + #using runqueue events as many as possible to update the progress bar + elif isinstance(event, bb.runqueue.runQueueTaskFailed): + self.emit("log", "error", "Task %s (%s) failed with exit code '%s'" % (event.taskid, event.taskstring, event.exitcode)) + elif isinstance(event, bb.runqueue.sceneQueueTaskFailed): + self.emit("log", "warn", "Setscene task %s (%s) failed with exit code '%s' - real task will be run instead" \ + % (event.taskid, event.taskstring, event.exitcode)) + elif isinstance(event, (bb.runqueue.runQueueTaskStarted, bb.runqueue.sceneQueueTaskStarted)): + if isinstance(event, bb.runqueue.sceneQueueTaskStarted): + self.emit("log", "info", "Running setscene task %d of %d (%s)" % \ + (event.stats.completed + event.stats.active + event.stats.failed + 1, + event.stats.total, event.taskstring)) + else: + if event.noexec: + tasktype = 'noexec task' + else: + tasktype = 'task' + self.emit("log", "info", "Running %s %s of %s (ID: %s, %s)" % \ + (tasktype, event.stats.completed + event.stats.active + event.stats.failed + 1, + event.stats.total, event.taskid, event.taskstring)) + message = {} + message["eventname"] = bb.event.getName(event) + num_of_completed = event.stats.completed + event.stats.failed + message["current"] = num_of_completed + message["total"] = event.stats.total + message["title"] = "" + message["task"] = event.taskstring + self.emit("task-started", message) + elif isinstance(event, bb.event.MultipleProviders): + self.emit("log", "info", "multiple providers are available for %s%s (%s)" \ + % (event._is_runtime and "runtime " or "", event._item, ", ".join(event._candidates))) + self.emit("log", "info", "consider defining a PREFERRED_PROVIDER entry to match %s" % (event._item)) + elif isinstance(event, bb.event.NoProvider): + msg = "" + if event._runtime: + r = "R" + else: + r = "" + + extra = '' + if not event._reasons: + if event._close_matches: + extra = ". Close matches:\n %s" % '\n '.join(event._close_matches) + + if event._dependees: + msg = "Nothing %sPROVIDES '%s' (but %s %sDEPENDS on or otherwise requires it)%s\n" % (r, event._item, ", ".join(event._dependees), r, extra) + else: + msg = "Nothing %sPROVIDES '%s'%s\n" % (r, event._item, extra) + if event._reasons: + for reason in event._reasons: + msg += ("%s\n" % reason) + self.emit("no-provider", msg) + self.emit("log", "error", msg) + elif isinstance(event, bb.event.LogExecTTY): + icon = "dialog-warning" + color = HobColors.WARNING + if self.sequential or not parent: + tree_add = self.model.append + else: + tree_add = self.model.prepend + tree_add(parent, + (None, + package, + task, + event.msg, + icon, + color, + 0)) + else: + if not isinstance(event, (bb.event.BuildBase, + bb.event.StampUpdate, + bb.event.ConfigParsed, + bb.event.RecipeParsed, + bb.event.RecipePreFinalise, + bb.runqueue.runQueueEvent, + bb.runqueue.runQueueExitWait, + bb.event.OperationStarted, + bb.event.OperationCompleted, + bb.event.OperationProgress)): + self.emit("log", "error", "Unknown event: %s" % (event.error if hasattr(event, 'error') else 'error')) + + return + + +def do_pastebin(text): + url = 'http://pastebin.com/api_public.php' + params = {'paste_code': text, 'paste_format': 'text'} + + req = urllib2.Request(url, urllib.urlencode(params)) + response = urllib2.urlopen(req) + paste_url = response.read() + + return paste_url + + +class RunningBuildTreeView (gtk.TreeView): + __gsignals__ = { + "button_press_event" : "override" + } + def __init__ (self, readonly=False, hob=False): + gtk.TreeView.__init__ (self) + self.readonly = readonly + + # The icon that indicates whether we're building or failed. + # add 'hob' flag because there has not only hob to share this code + if hob: + renderer = HobCellRendererPixbuf () + else: + renderer = gtk.CellRendererPixbuf() + col = gtk.TreeViewColumn ("Status", renderer) + col.add_attribute (renderer, "icon-name", 4) + self.append_column (col) + + # The message of the build. + # add 'hob' flag because there has not only hob to share this code + if hob: + self.message_renderer = HobWarpCellRendererText (col_number=1) + else: + self.message_renderer = gtk.CellRendererText () + self.message_column = gtk.TreeViewColumn ("Message", self.message_renderer, text=3) + self.message_column.add_attribute(self.message_renderer, 'background', 5) + self.message_renderer.set_property('editable', (not self.readonly)) + self.append_column (self.message_column) + + def do_button_press_event(self, event): + gtk.TreeView.do_button_press_event(self, event) + + if event.button == 3: + selection = super(RunningBuildTreeView, self).get_selection() + (model, it) = selection.get_selected() + if it is not None: + can_paste = model.get(it, model.COL_LOG)[0] + if can_paste == 'pastebin': + # build a simple menu with a pastebin option + menu = gtk.Menu() + menuitem = gtk.MenuItem("Copy") + menu.append(menuitem) + menuitem.connect("activate", self.clipboard_handler, (model, it)) + menuitem.show() + menuitem = gtk.MenuItem("Send log to pastebin") + menu.append(menuitem) + menuitem.connect("activate", self.pastebin_handler, (model, it)) + menuitem.show() + menu.show() + menu.popup(None, None, None, event.button, event.time) + + def _add_to_clipboard(self, clipping): + """ + Add the contents of clipping to the system clipboard. + """ + clipboard = gtk.clipboard_get() + clipboard.set_text(clipping) + clipboard.store() + + def pastebin_handler(self, widget, data): + """ + Send the log data to pastebin, then add the new paste url to the + clipboard. + """ + (model, it) = data + paste_url = do_pastebin(model.get(it, model.COL_MESSAGE)[0]) + + # @todo Provide visual feedback to the user that it is done and that + # it worked. + print paste_url + + self._add_to_clipboard(paste_url) + + def clipboard_handler(self, widget, data): + """ + """ + (model, it) = data + message = model.get(it, model.COL_MESSAGE)[0] + + self._add_to_clipboard(message) + +class BuildFailureTreeView(gtk.TreeView): + + def __init__ (self): + gtk.TreeView.__init__(self) + self.set_rules_hint(False) + self.set_headers_visible(False) + self.get_selection().set_mode(gtk.SELECTION_SINGLE) + + # The icon that indicates whether we're building or failed. + renderer = HobCellRendererPixbuf () + col = gtk.TreeViewColumn ("Status", renderer) + col.add_attribute (renderer, "icon-name", RunningBuildModel.COL_ICON) + self.append_column (col) + + # The message of the build. + self.message_renderer = HobWarpCellRendererText (col_number=1) + self.message_column = gtk.TreeViewColumn ("Message", self.message_renderer, text=RunningBuildModel.COL_MESSAGE, background=RunningBuildModel.COL_COLOR) + self.append_column (self.message_column) diff --git a/bitbake/lib/bb/ui/crumbs/sanitycheckpage.py b/bitbake/lib/bb/ui/crumbs/sanitycheckpage.py new file mode 100644 index 0000000000..76ce2ecc23 --- /dev/null +++ b/bitbake/lib/bb/ui/crumbs/sanitycheckpage.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python +# +# BitBake Graphical GTK User Interface +# +# Copyright (C) 2012 Intel Corporation +# +# Authored by Bogdan Marinescu +# +# 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, gobject +from bb.ui.crumbs.progressbar import HobProgressBar +from bb.ui.crumbs.hobwidget import hic +from bb.ui.crumbs.hobpages import HobPage + +# +# SanityCheckPage +# +class SanityCheckPage (HobPage): + + def __init__(self, builder): + super(SanityCheckPage, self).__init__(builder) + self.running = False + self.create_visual_elements() + self.show_all() + + def make_label(self, text, bold=True): + label = gtk.Label() + label.set_alignment(0.0, 0.5) + mark = "%s" % (self.span_tag('x-large', 'bold') if bold else self.span_tag('medium'), text) + label.set_markup(mark) + return label + + def start(self): + if not self.running: + self.running = True + gobject.timeout_add(100, self.timer_func) + + def stop(self): + self.running = False + + def is_running(self): + return self.running + + def timer_func(self): + self.progress_bar.pulse() + return self.running + + def create_visual_elements(self): + # Table'd layout. 'rows' and 'cols' give the table size + rows, cols = 30, 50 + self.table = gtk.Table(rows, cols, True) + self.pack_start(self.table, expand=False, fill=False) + sx, sy = 2, 2 + # 'info' icon + image = gtk.Image() + image.set_from_file(hic.ICON_INFO_DISPLAY_FILE) + self.table.attach(image, sx, sx + 2, sy, sy + 3 ) + image.show() + # 'Checking' message + label = self.make_label('Hob is checking for correct build system setup') + self.table.attach(label, sx + 2, cols, sy, sy + 3, xpadding=5 ) + label.show() + # 'Shouldn't take long' message. + label = self.make_label("The check shouldn't take long.", False) + self.table.attach(label, sx + 2, cols, sy + 3, sy + 4, xpadding=5) + label.show() + # Progress bar + self.progress_bar = HobProgressBar() + self.table.attach(self.progress_bar, sx + 2, cols - 3, sy + 5, sy + 7, xpadding=5) + self.progress_bar.show() + # All done + self.table.show() + diff --git a/bitbake/lib/bb/ui/crumbs/utils.py b/bitbake/lib/bb/ui/crumbs/utils.py new file mode 100644 index 0000000000..939864fa6f --- /dev/null +++ b/bitbake/lib/bb/ui/crumbs/utils.py @@ -0,0 +1,34 @@ +# +# BitBake UI Utils +# +# Copyright (C) 2012 Intel Corporation +# +# 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. + +# This utility method looks for xterm or vte and return the +# frist to exist, currently we are keeping this simple, but +# we will likely move the oe.terminal implementation into +# bitbake which will allow more flexibility. + +import os +import bb + +def which_terminal(): + term = bb.utils.which(os.environ["PATH"], "xterm") + if term: + return term + " -e " + term = bb.utils.which(os.environ["PATH"], "vte") + if term: + return term + " -c " + return None diff --git a/bitbake/lib/bb/ui/depexp.py b/bitbake/lib/bb/ui/depexp.py new file mode 100644 index 0000000000..4578dce615 --- /dev/null +++ b/bitbake/lib/bb/ui/depexp.py @@ -0,0 +1,326 @@ +# +# BitBake Graphical GTK based Dependency Explorer +# +# Copyright (C) 2007 Ross Burton +# Copyright (C) 2007 - 2008 Richard Purdie +# +# 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 gobject +import gtk +import Queue +import threading +import xmlrpclib +import bb +import bb.event +from bb.ui.crumbs.progressbar import HobProgressBar + +# Package Model +(COL_PKG_NAME) = (0) + +# Dependency Model +(TYPE_DEP, TYPE_RDEP) = (0, 1) +(COL_DEP_TYPE, COL_DEP_PARENT, COL_DEP_PACKAGE) = (0, 1, 2) + + +class PackageDepView(gtk.TreeView): + def __init__(self, model, dep_type, label): + gtk.TreeView.__init__(self) + self.current = None + self.dep_type = dep_type + self.filter_model = model.filter_new() + self.filter_model.set_visible_func(self._filter) + self.set_model(self.filter_model) + #self.connect("row-activated", self.on_package_activated, COL_DEP_PACKAGE) + self.append_column(gtk.TreeViewColumn(label, gtk.CellRendererText(), text=COL_DEP_PACKAGE)) + + def _filter(self, model, iter): + (this_type, package) = model.get(iter, COL_DEP_TYPE, COL_DEP_PARENT) + if this_type != self.dep_type: return False + return package == self.current + + def set_current_package(self, package): + self.current = package + self.filter_model.refilter() + + +class PackageReverseDepView(gtk.TreeView): + def __init__(self, model, label): + gtk.TreeView.__init__(self) + self.current = None + self.filter_model = model.filter_new() + self.filter_model.set_visible_func(self._filter) + self.set_model(self.filter_model) + self.append_column(gtk.TreeViewColumn(label, gtk.CellRendererText(), text=COL_DEP_PARENT)) + + def _filter(self, model, iter): + package = model.get_value(iter, COL_DEP_PACKAGE) + return package == self.current + + def set_current_package(self, package): + self.current = package + self.filter_model.refilter() + + +class DepExplorer(gtk.Window): + def __init__(self): + gtk.Window.__init__(self) + self.set_title("Dependency Explorer") + self.set_default_size(500, 500) + self.connect("delete-event", gtk.main_quit) + + # Create the data models + self.pkg_model = gtk.ListStore(gobject.TYPE_STRING) + self.pkg_model.set_sort_column_id(COL_PKG_NAME, gtk.SORT_ASCENDING) + self.depends_model = gtk.ListStore(gobject.TYPE_INT, gobject.TYPE_STRING, gobject.TYPE_STRING) + self.depends_model.set_sort_column_id(COL_DEP_PACKAGE, gtk.SORT_ASCENDING) + + pane = gtk.HPaned() + pane.set_position(250) + self.add(pane) + + # The master list of packages + scrolled = gtk.ScrolledWindow() + scrolled.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) + scrolled.set_shadow_type(gtk.SHADOW_IN) + + self.pkg_treeview = gtk.TreeView(self.pkg_model) + self.pkg_treeview.get_selection().connect("changed", self.on_cursor_changed) + column = gtk.TreeViewColumn("Package", gtk.CellRendererText(), text=COL_PKG_NAME) + self.pkg_treeview.append_column(column) + pane.add1(scrolled) + scrolled.add(self.pkg_treeview) + + box = gtk.VBox(homogeneous=True, spacing=4) + + # Runtime Depends + scrolled = gtk.ScrolledWindow() + scrolled.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) + scrolled.set_shadow_type(gtk.SHADOW_IN) + self.rdep_treeview = PackageDepView(self.depends_model, TYPE_RDEP, "Runtime Depends") + self.rdep_treeview.connect("row-activated", self.on_package_activated, COL_DEP_PACKAGE) + scrolled.add(self.rdep_treeview) + box.add(scrolled) + + # Build Depends + scrolled = gtk.ScrolledWindow() + scrolled.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) + scrolled.set_shadow_type(gtk.SHADOW_IN) + self.dep_treeview = PackageDepView(self.depends_model, TYPE_DEP, "Build Depends") + self.dep_treeview.connect("row-activated", self.on_package_activated, COL_DEP_PACKAGE) + scrolled.add(self.dep_treeview) + box.add(scrolled) + pane.add2(box) + + # Reverse Depends + scrolled = gtk.ScrolledWindow() + scrolled.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) + scrolled.set_shadow_type(gtk.SHADOW_IN) + self.revdep_treeview = PackageReverseDepView(self.depends_model, "Reverse Depends") + self.revdep_treeview.connect("row-activated", self.on_package_activated, COL_DEP_PARENT) + scrolled.add(self.revdep_treeview) + box.add(scrolled) + pane.add2(box) + + self.show_all() + + def on_package_activated(self, treeview, path, column, data_col): + model = treeview.get_model() + package = model.get_value(model.get_iter(path), data_col) + + pkg_path = [] + def finder(model, path, iter, needle): + package = model.get_value(iter, COL_PKG_NAME) + if package == needle: + pkg_path.append(path) + return True + else: + return False + self.pkg_model.foreach(finder, package) + if pkg_path: + self.pkg_treeview.get_selection().select_path(pkg_path[0]) + self.pkg_treeview.scroll_to_cell(pkg_path[0]) + + def on_cursor_changed(self, selection): + (model, it) = selection.get_selected() + if it is None: + current_package = None + else: + current_package = model.get_value(it, COL_PKG_NAME) + self.rdep_treeview.set_current_package(current_package) + self.dep_treeview.set_current_package(current_package) + self.revdep_treeview.set_current_package(current_package) + + + def parse(self, depgraph): + for package in depgraph["pn"]: + self.pkg_model.insert(0, (package,)) + + for package in depgraph["depends"]: + for depend in depgraph["depends"][package]: + self.depends_model.insert (0, (TYPE_DEP, package, depend)) + + for package in depgraph["rdepends-pn"]: + for rdepend in depgraph["rdepends-pn"][package]: + self.depends_model.insert (0, (TYPE_RDEP, package, rdepend)) + + +class gtkthread(threading.Thread): + quit = threading.Event() + def __init__(self, shutdown): + threading.Thread.__init__(self) + self.setDaemon(True) + self.shutdown = shutdown + + def run(self): + gobject.threads_init() + gtk.gdk.threads_init() + gtk.main() + gtkthread.quit.set() + + +def main(server, eventHandler, params): + try: + params.updateFromServer(server) + cmdline = params.parseActions() + if not cmdline: + print("Nothing to do. Use 'bitbake world' to build everything, or run 'bitbake --help' for usage information.") + return 1 + if 'msg' in cmdline and cmdline['msg']: + logger.error(cmdline['msg']) + return 1 + cmdline = cmdline['action'] + if not cmdline or cmdline[0] != "generateDotGraph": + print("This UI requires the -g option") + return 1 + ret, error = server.runCommand(["generateDepTreeEvent", cmdline[1], cmdline[2]]) + if error: + print("Error running command '%s': %s" % (cmdline, error)) + return 1 + elif ret != True: + print("Error running command '%s': returned %s" % (cmdline, ret)) + return 1 + except xmlrpclib.Fault as x: + print("XMLRPC Fault getting commandline:\n %s" % x) + return + + shutdown = 0 + + gtkgui = gtkthread(shutdown) + gtkgui.start() + + gtk.gdk.threads_enter() + dep = DepExplorer() + bardialog = gtk.Dialog(parent=dep, + flags=gtk.DIALOG_MODAL|gtk.DIALOG_DESTROY_WITH_PARENT) + bardialog.set_default_size(400, 50) + pbar = HobProgressBar() + bardialog.vbox.pack_start(pbar) + bardialog.show_all() + bardialog.connect("delete-event", gtk.main_quit) + gtk.gdk.threads_leave() + + progress_total = 0 + while True: + try: + event = eventHandler.waitEvent(0.25) + if gtkthread.quit.isSet(): + _, error = server.runCommand(["stateStop"]) + if error: + print('Unable to cleanly stop: %s' % error) + break + + if event is None: + continue + + if isinstance(event, bb.event.CacheLoadStarted): + progress_total = event.total + gtk.gdk.threads_enter() + bardialog.set_title("Loading Cache") + pbar.update(0) + gtk.gdk.threads_leave() + + if isinstance(event, bb.event.CacheLoadProgress): + x = event.current + gtk.gdk.threads_enter() + pbar.update(x * 1.0 / progress_total) + pbar.set_title('') + gtk.gdk.threads_leave() + continue + + if isinstance(event, bb.event.CacheLoadCompleted): + bardialog.hide() + continue + + if isinstance(event, bb.event.ParseStarted): + progress_total = event.total + if progress_total == 0: + continue + gtk.gdk.threads_enter() + pbar.update(0) + bardialog.set_title("Processing recipes") + + gtk.gdk.threads_leave() + + if isinstance(event, bb.event.ParseProgress): + x = event.current + gtk.gdk.threads_enter() + pbar.update(x * 1.0 / progress_total) + pbar.set_title('') + gtk.gdk.threads_leave() + continue + + if isinstance(event, bb.event.ParseCompleted): + bardialog.hide() + continue + + if isinstance(event, bb.event.DepTreeGenerated): + gtk.gdk.threads_enter() + dep.parse(event._depgraph) + gtk.gdk.threads_leave() + + if isinstance(event, bb.command.CommandCompleted): + continue + + if isinstance(event, bb.command.CommandFailed): + print("Command execution failed: %s" % event.error) + return event.exitcode + + if isinstance(event, bb.command.CommandExit): + return event.exitcode + + if isinstance(event, bb.cooker.CookerExit): + break + + continue + except EnvironmentError as ioerror: + # ignore interrupted io + if ioerror.args[0] == 4: + pass + except KeyboardInterrupt: + if shutdown == 2: + print("\nThird Keyboard Interrupt, exit.\n") + break + if shutdown == 1: + print("\nSecond Keyboard Interrupt, stopping...\n") + _, error = server.runCommand(["stateForceShutdown"]) + if error: + print('Unable to cleanly stop: %s' % error) + if shutdown == 0: + print("\nKeyboard Interrupt, closing down...\n") + _, error = server.runCommand(["stateShutdown"]) + if error: + print('Unable to cleanly shutdown: %s' % error) + shutdown = shutdown + 1 + pass diff --git a/bitbake/lib/bb/ui/goggle.py b/bitbake/lib/bb/ui/goggle.py new file mode 100644 index 0000000000..f4ee7b41ae --- /dev/null +++ b/bitbake/lib/bb/ui/goggle.py @@ -0,0 +1,121 @@ +# +# BitBake Graphical GTK User Interface +# +# Copyright (C) 2008 Intel Corporation +# +# Authored by Rob Bradford +# +# 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 gobject +import gtk +import xmlrpclib +from bb.ui.crumbs.runningbuild import RunningBuildTreeView, RunningBuild +from bb.ui.crumbs.progress import ProgressBar + +import Queue + + +def event_handle_idle_func (eventHandler, build, pbar): + + # Consume as many messages as we can in the time available to us + event = eventHandler.getEvent() + while event: + build.handle_event (event, pbar) + event = eventHandler.getEvent() + + return True + +def scroll_tv_cb (model, path, iter, view): + view.scroll_to_cell (path) + + +# @todo hook these into the GUI so the user has feedback... +def running_build_failed_cb (running_build): + pass + + +def running_build_succeeded_cb (running_build): + pass + + +class MainWindow (gtk.Window): + def __init__ (self): + gtk.Window.__init__ (self, gtk.WINDOW_TOPLEVEL) + + # Setup tree view and the scrolled window + scrolled_window = gtk.ScrolledWindow () + self.add (scrolled_window) + self.cur_build_tv = RunningBuildTreeView() + self.connect("delete-event", gtk.main_quit) + self.set_default_size(640, 480) + scrolled_window.add (self.cur_build_tv) + + +def main (server, eventHandler, params): + gobject.threads_init() + gtk.gdk.threads_init() + + window = MainWindow () + window.show_all () + pbar = ProgressBar(window) + pbar.connect("delete-event", gtk.main_quit) + + # Create the object for the current build + running_build = RunningBuild () + window.cur_build_tv.set_model (running_build.model) + running_build.model.connect("row-inserted", scroll_tv_cb, window.cur_build_tv) + running_build.connect ("build-succeeded", running_build_succeeded_cb) + running_build.connect ("build-failed", running_build_failed_cb) + + try: + params.updateFromServer(server) + cmdline = params.parseActions() + if not cmdline: + print("Nothing to do. Use 'bitbake world' to build everything, or run 'bitbake --help' for usage information.") + return 1 + if 'msg' in cmdline and cmdline['msg']: + logger.error(cmdline['msg']) + return 1 + cmdline = cmdline['action'] + ret, error = server.runCommand(cmdline) + if error: + print("Error running command '%s': %s" % (cmdline, error)) + return 1 + elif ret != True: + print("Error running command '%s': returned %s" % (cmdline, ret)) + return 1 + except xmlrpclib.Fault as x: + print("XMLRPC Fault getting commandline:\n %s" % x) + return 1 + + # Use a timeout function for probing the event queue to find out if we + # have a message waiting for us. + gobject.timeout_add (100, + event_handle_idle_func, + eventHandler, + running_build, + pbar) + + try: + gtk.main() + except EnvironmentError as ioerror: + # ignore interrupted io + if ioerror.args[0] == 4: + pass + except KeyboardInterrupt: + pass + finally: + server.runCommand(["stateForceShutdown"]) + diff --git a/bitbake/lib/bb/ui/hob.py b/bitbake/lib/bb/ui/hob.py new file mode 100755 index 0000000000..da5b411891 --- /dev/null +++ b/bitbake/lib/bb/ui/hob.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python +# +# BitBake Graphical GTK User Interface +# +# Copyright (C) 2011 Intel Corporation +# +# Authored by Joshua Lock +# Authored by Dongxiao Xu +# +# 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 sys +import os +requirements = "FATAL: Hob requires Gtk+ 2.20.0 or higher, PyGtk 2.21.0 or higher" +try: + import gobject + import gtk + import pygtk + pygtk.require('2.0') # to be certain we don't have gtk+ 1.x !?! + gtkver = gtk.gtk_version + pygtkver = gtk.pygtk_version + if gtkver < (2, 20, 0) or pygtkver < (2, 21, 0): + sys.exit("%s,\nYou have Gtk+ %s and PyGtk %s." % (requirements, + ".".join(map(str, gtkver)), + ".".join(map(str, pygtkver)))) +except ImportError as exc: + sys.exit("%s (%s)." % (requirements, str(exc))) +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(__file__)))) +try: + import bb +except RuntimeError as exc: + sys.exit(str(exc)) +from bb.ui import uihelper +from bb.ui.crumbs.hoblistmodel import RecipeListModel, PackageListModel +from bb.ui.crumbs.hobeventhandler import HobHandler +from bb.ui.crumbs.builder import Builder + +featureSet = [bb.cooker.CookerFeatures.HOB_EXTRA_CACHES, bb.cooker.CookerFeatures.BASEDATASTORE_TRACKING] + +def event_handle_idle_func(eventHandler, hobHandler): + # Consume as many messages as we can in the time available to us + if not eventHandler: + return False + event = eventHandler.getEvent() + while event: + hobHandler.handle_event(event) + event = eventHandler.getEvent() + return True + +_evt_list = [ "bb.runqueue.runQueueExitWait", "bb.event.LogExecTTY", "logging.LogRecord", + "bb.build.TaskFailed", "bb.build.TaskBase", "bb.event.ParseStarted", + "bb.event.ParseProgress", "bb.event.ParseCompleted", "bb.event.CacheLoadStarted", + "bb.event.CacheLoadProgress", "bb.event.CacheLoadCompleted", "bb.command.CommandFailed", + "bb.command.CommandExit", "bb.command.CommandCompleted", "bb.cooker.CookerExit", + "bb.event.MultipleProviders", "bb.event.NoProvider", "bb.runqueue.sceneQueueTaskStarted", + "bb.runqueue.runQueueTaskStarted", "bb.runqueue.runQueueTaskFailed", "bb.runqueue.sceneQueueTaskFailed", + "bb.event.BuildBase", "bb.build.TaskStarted", "bb.build.TaskSucceeded", "bb.build.TaskFailedSilent", + "bb.event.SanityCheckPassed", "bb.event.SanityCheckFailed", "bb.event.PackageInfo", + "bb.event.TargetsTreeGenerated", "bb.event.ConfigFilesFound", "bb.event.ConfigFilePathFound", + "bb.event.FilesMatchingFound", "bb.event.NetworkTestFailed", "bb.event.NetworkTestPassed", + "bb.event.BuildStarted", "bb.event.BuildCompleted", "bb.event.DiskFull"] + +def main (server, eventHandler, params): + params.updateFromServer(server) + gobject.threads_init() + + # That indicates whether the Hob and the bitbake server are + # running on different machines + # recipe model and package model + recipe_model = RecipeListModel() + package_model = PackageListModel() + + llevel, debug_domains = bb.msg.constructLogOptions() + server.runCommand(["setEventMask", server.getEventHandle(), llevel, debug_domains, _evt_list]) + hobHandler = HobHandler(server, recipe_model, package_model) + builder = Builder(hobHandler, recipe_model, package_model) + + # This timeout function regularly probes the event queue to find out if we + # have any messages waiting for us. + gobject.timeout_add(10, event_handle_idle_func, eventHandler, hobHandler) + + try: + gtk.main() + except EnvironmentError as ioerror: + # ignore interrupted io + if ioerror.args[0] == 4: + pass + finally: + hobHandler.cancel_build(force = True) + +if __name__ == "__main__": + try: + ret = main() + except Exception: + ret = 1 + import traceback + traceback.print_exc(15) + sys.exit(ret) diff --git a/bitbake/lib/bb/ui/icons/images/images_display.png b/bitbake/lib/bb/ui/icons/images/images_display.png new file mode 100644 index 0000000000..a7f87101af Binary files /dev/null and b/bitbake/lib/bb/ui/icons/images/images_display.png differ diff --git a/bitbake/lib/bb/ui/icons/images/images_hover.png b/bitbake/lib/bb/ui/icons/images/images_hover.png new file mode 100644 index 0000000000..2d9cd99b8e Binary files /dev/null and b/bitbake/lib/bb/ui/icons/images/images_hover.png differ diff --git a/bitbake/lib/bb/ui/icons/indicators/add-hover.png b/bitbake/lib/bb/ui/icons/indicators/add-hover.png new file mode 100644 index 0000000000..526df770d1 Binary files /dev/null and b/bitbake/lib/bb/ui/icons/indicators/add-hover.png differ diff --git a/bitbake/lib/bb/ui/icons/indicators/add.png b/bitbake/lib/bb/ui/icons/indicators/add.png new file mode 100644 index 0000000000..31e7090d61 Binary files /dev/null and b/bitbake/lib/bb/ui/icons/indicators/add.png differ diff --git a/bitbake/lib/bb/ui/icons/indicators/alert.png b/bitbake/lib/bb/ui/icons/indicators/alert.png new file mode 100644 index 0000000000..d1c6f55a2f Binary files /dev/null and b/bitbake/lib/bb/ui/icons/indicators/alert.png differ diff --git a/bitbake/lib/bb/ui/icons/indicators/confirmation.png b/bitbake/lib/bb/ui/icons/indicators/confirmation.png new file mode 100644 index 0000000000..3a5402d1e3 Binary files /dev/null and b/bitbake/lib/bb/ui/icons/indicators/confirmation.png differ diff --git a/bitbake/lib/bb/ui/icons/indicators/denied.png b/bitbake/lib/bb/ui/icons/indicators/denied.png new file mode 100644 index 0000000000..ee35c7defa Binary files /dev/null and b/bitbake/lib/bb/ui/icons/indicators/denied.png differ diff --git a/bitbake/lib/bb/ui/icons/indicators/error.png b/bitbake/lib/bb/ui/icons/indicators/error.png new file mode 100644 index 0000000000..d06a8c151a Binary files /dev/null and b/bitbake/lib/bb/ui/icons/indicators/error.png differ diff --git a/bitbake/lib/bb/ui/icons/indicators/info.png b/bitbake/lib/bb/ui/icons/indicators/info.png new file mode 100644 index 0000000000..ee8e8d8462 Binary files /dev/null and b/bitbake/lib/bb/ui/icons/indicators/info.png differ diff --git a/bitbake/lib/bb/ui/icons/indicators/issues.png b/bitbake/lib/bb/ui/icons/indicators/issues.png new file mode 100644 index 0000000000..b0c7461334 Binary files /dev/null and b/bitbake/lib/bb/ui/icons/indicators/issues.png differ diff --git a/bitbake/lib/bb/ui/icons/indicators/refresh.png b/bitbake/lib/bb/ui/icons/indicators/refresh.png new file mode 100644 index 0000000000..eb6c419db8 Binary files /dev/null and b/bitbake/lib/bb/ui/icons/indicators/refresh.png differ diff --git a/bitbake/lib/bb/ui/icons/indicators/remove-hover.png b/bitbake/lib/bb/ui/icons/indicators/remove-hover.png new file mode 100644 index 0000000000..aa57c69982 Binary files /dev/null and b/bitbake/lib/bb/ui/icons/indicators/remove-hover.png differ diff --git a/bitbake/lib/bb/ui/icons/indicators/remove.png b/bitbake/lib/bb/ui/icons/indicators/remove.png new file mode 100644 index 0000000000..05c3c293d4 Binary files /dev/null and b/bitbake/lib/bb/ui/icons/indicators/remove.png differ diff --git a/bitbake/lib/bb/ui/icons/indicators/tick.png b/bitbake/lib/bb/ui/icons/indicators/tick.png new file mode 100644 index 0000000000..beaad361c3 Binary files /dev/null and b/bitbake/lib/bb/ui/icons/indicators/tick.png differ diff --git a/bitbake/lib/bb/ui/icons/info/info_display.png b/bitbake/lib/bb/ui/icons/info/info_display.png new file mode 100644 index 0000000000..5afbba29f5 Binary files /dev/null and b/bitbake/lib/bb/ui/icons/info/info_display.png differ diff --git a/bitbake/lib/bb/ui/icons/info/info_hover.png b/bitbake/lib/bb/ui/icons/info/info_hover.png new file mode 100644 index 0000000000..f9d294dfae Binary files /dev/null and b/bitbake/lib/bb/ui/icons/info/info_hover.png differ diff --git a/bitbake/lib/bb/ui/icons/layers/layers_display.png b/bitbake/lib/bb/ui/icons/layers/layers_display.png new file mode 100644 index 0000000000..b7f9053a9e Binary files /dev/null and b/bitbake/lib/bb/ui/icons/layers/layers_display.png differ diff --git a/bitbake/lib/bb/ui/icons/layers/layers_hover.png b/bitbake/lib/bb/ui/icons/layers/layers_hover.png new file mode 100644 index 0000000000..0bf3ce0dbc Binary files /dev/null and b/bitbake/lib/bb/ui/icons/layers/layers_hover.png differ diff --git a/bitbake/lib/bb/ui/icons/packages/packages_display.png b/bitbake/lib/bb/ui/icons/packages/packages_display.png new file mode 100644 index 0000000000..f5d0a5064d Binary files /dev/null and b/bitbake/lib/bb/ui/icons/packages/packages_display.png differ diff --git a/bitbake/lib/bb/ui/icons/packages/packages_hover.png b/bitbake/lib/bb/ui/icons/packages/packages_hover.png new file mode 100644 index 0000000000..c081165f34 Binary files /dev/null and b/bitbake/lib/bb/ui/icons/packages/packages_hover.png differ diff --git a/bitbake/lib/bb/ui/icons/recipe/recipe_display.png b/bitbake/lib/bb/ui/icons/recipe/recipe_display.png new file mode 100644 index 0000000000..e9809bc7d9 Binary files /dev/null and b/bitbake/lib/bb/ui/icons/recipe/recipe_display.png differ diff --git a/bitbake/lib/bb/ui/icons/recipe/recipe_hover.png b/bitbake/lib/bb/ui/icons/recipe/recipe_hover.png new file mode 100644 index 0000000000..7e48da9af0 Binary files /dev/null and b/bitbake/lib/bb/ui/icons/recipe/recipe_hover.png differ diff --git a/bitbake/lib/bb/ui/icons/settings/settings_display.png b/bitbake/lib/bb/ui/icons/settings/settings_display.png new file mode 100644 index 0000000000..88c464db04 Binary files /dev/null and b/bitbake/lib/bb/ui/icons/settings/settings_display.png differ diff --git a/bitbake/lib/bb/ui/icons/settings/settings_hover.png b/bitbake/lib/bb/ui/icons/settings/settings_hover.png new file mode 100644 index 0000000000..d92a0bf2c3 Binary files /dev/null and b/bitbake/lib/bb/ui/icons/settings/settings_hover.png differ diff --git a/bitbake/lib/bb/ui/icons/templates/templates_display.png b/bitbake/lib/bb/ui/icons/templates/templates_display.png new file mode 100644 index 0000000000..153c7afb62 Binary files /dev/null and b/bitbake/lib/bb/ui/icons/templates/templates_display.png differ diff --git a/bitbake/lib/bb/ui/icons/templates/templates_hover.png b/bitbake/lib/bb/ui/icons/templates/templates_hover.png new file mode 100644 index 0000000000..afb7165fe5 Binary files /dev/null and b/bitbake/lib/bb/ui/icons/templates/templates_hover.png differ diff --git a/bitbake/lib/bb/ui/knotty.py b/bitbake/lib/bb/ui/knotty.py new file mode 100644 index 0000000000..41f1ba83af --- /dev/null +++ b/bitbake/lib/bb/ui/knotty.py @@ -0,0 +1,550 @@ +# +# BitBake (No)TTY UI Implementation +# +# Handling output to TTYs or files (no TTY) +# +# Copyright (C) 2006-2012 Richard Purdie +# +# 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. + +from __future__ import division + +import os +import sys +import xmlrpclib +import logging +import progressbar +import signal +import bb.msg +import time +import fcntl +import struct +import copy +import atexit +from bb.ui import uihelper + +featureSet = [bb.cooker.CookerFeatures.SEND_SANITYEVENTS] + +logger = logging.getLogger("BitBake") +interactive = sys.stdout.isatty() + +class BBProgress(progressbar.ProgressBar): + def __init__(self, msg, maxval): + self.msg = msg + widgets = [progressbar.Percentage(), ' ', progressbar.Bar(), ' ', + progressbar.ETA()] + + try: + self._resize_default = signal.getsignal(signal.SIGWINCH) + except: + self._resize_default = None + progressbar.ProgressBar.__init__(self, maxval, [self.msg + ": "] + widgets, fd=sys.stdout) + + def _handle_resize(self, signum, frame): + progressbar.ProgressBar._handle_resize(self, signum, frame) + if self._resize_default: + self._resize_default(signum, frame) + def finish(self): + progressbar.ProgressBar.finish(self) + if self._resize_default: + signal.signal(signal.SIGWINCH, self._resize_default) + +class NonInteractiveProgress(object): + fobj = sys.stdout + + def __init__(self, msg, maxval): + self.msg = msg + self.maxval = maxval + + def start(self): + self.fobj.write("%s..." % self.msg) + self.fobj.flush() + return self + + def update(self, value): + pass + + def finish(self): + self.fobj.write("done.\n") + self.fobj.flush() + +def new_progress(msg, maxval): + if interactive: + return BBProgress(msg, maxval) + else: + return NonInteractiveProgress(msg, maxval) + +def pluralise(singular, plural, qty): + if(qty == 1): + return singular % qty + else: + return plural % qty + + +class InteractConsoleLogFilter(logging.Filter): + def __init__(self, tf, format): + self.tf = tf + self.format = format + + def filter(self, record): + if record.levelno == self.format.NOTE and (record.msg.startswith("Running") or record.msg.startswith("recipe ")): + return False + self.tf.clearFooter() + return True + +class TerminalFilter(object): + columns = 80 + + def sigwinch_handle(self, signum, frame): + self.columns = self.getTerminalColumns() + if self._sigwinch_default: + self._sigwinch_default(signum, frame) + + def getTerminalColumns(self): + def ioctl_GWINSZ(fd): + try: + cr = struct.unpack('hh', fcntl.ioctl(fd, self.termios.TIOCGWINSZ, '1234')) + except: + return None + return cr + cr = ioctl_GWINSZ(sys.stdout.fileno()) + if not cr: + try: + fd = os.open(os.ctermid(), os.O_RDONLY) + cr = ioctl_GWINSZ(fd) + os.close(fd) + except: + pass + if not cr: + try: + cr = (env['LINES'], env['COLUMNS']) + except: + cr = (25, 80) + return cr[1] + + def __init__(self, main, helper, console, errconsole, format): + self.main = main + self.helper = helper + self.cuu = None + self.stdinbackup = None + self.interactive = sys.stdout.isatty() + self.footer_present = False + self.lastpids = [] + + if not self.interactive: + return + + try: + import curses + except ImportError: + sys.exit("FATAL: The knotty ui could not load the required curses python module.") + + import termios + self.curses = curses + self.termios = termios + try: + fd = sys.stdin.fileno() + self.stdinbackup = termios.tcgetattr(fd) + new = copy.deepcopy(self.stdinbackup) + new[3] = new[3] & ~termios.ECHO + termios.tcsetattr(fd, termios.TCSADRAIN, new) + curses.setupterm() + if curses.tigetnum("colors") > 2: + format.enable_color() + self.ed = curses.tigetstr("ed") + if self.ed: + self.cuu = curses.tigetstr("cuu") + try: + self._sigwinch_default = signal.getsignal(signal.SIGWINCH) + signal.signal(signal.SIGWINCH, self.sigwinch_handle) + except: + pass + self.columns = self.getTerminalColumns() + except: + self.cuu = None + console.addFilter(InteractConsoleLogFilter(self, format)) + errconsole.addFilter(InteractConsoleLogFilter(self, format)) + + def clearFooter(self): + if self.footer_present: + lines = self.footer_present + sys.stdout.write(self.curses.tparm(self.cuu, lines)) + sys.stdout.write(self.curses.tparm(self.ed)) + self.footer_present = False + + def updateFooter(self): + if not self.cuu: + return + activetasks = self.helper.running_tasks + failedtasks = self.helper.failed_tasks + runningpids = self.helper.running_pids + if self.footer_present and (self.lastcount == self.helper.tasknumber_current) and (self.lastpids == runningpids): + return + if self.footer_present: + self.clearFooter() + if (not self.helper.tasknumber_total or self.helper.tasknumber_current == self.helper.tasknumber_total) and not len(activetasks): + return + tasks = [] + for t in runningpids: + tasks.append("%s (pid %s)" % (activetasks[t]["title"], t)) + + if self.main.shutdown: + content = "Waiting for %s running tasks to finish:" % len(activetasks) + elif not len(activetasks): + content = "No currently running tasks (%s of %s)" % (self.helper.tasknumber_current, self.helper.tasknumber_total) + else: + content = "Currently %s running tasks (%s of %s):" % (len(activetasks), self.helper.tasknumber_current, self.helper.tasknumber_total) + print(content) + lines = 1 + int(len(content) / (self.columns + 1)) + for tasknum, task in enumerate(tasks): + content = "%s: %s" % (tasknum, task) + print(content) + lines = lines + 1 + int(len(content) / (self.columns + 1)) + self.footer_present = lines + self.lastpids = runningpids[:] + self.lastcount = self.helper.tasknumber_current + + def finish(self): + if self.stdinbackup: + fd = sys.stdin.fileno() + self.termios.tcsetattr(fd, self.termios.TCSADRAIN, self.stdinbackup) + +def _log_settings_from_server(server): + # Get values of variables which control our output + includelogs, error = server.runCommand(["getVariable", "BBINCLUDELOGS"]) + if error: + logger.error("Unable to get the value of BBINCLUDELOGS variable: %s" % error) + raise BaseException(error) + loglines, error = server.runCommand(["getVariable", "BBINCLUDELOGS_LINES"]) + if error: + logger.error("Unable to get the value of BBINCLUDELOGS_LINES variable: %s" % error) + raise BaseException(error) + consolelogfile, error = server.runCommand(["getVariable", "BB_CONSOLELOG"]) + if error: + logger.error("Unable to get the value of BB_CONSOLELOG variable: %s" % error) + raise BaseException(error) + return includelogs, loglines, consolelogfile + +_evt_list = [ "bb.runqueue.runQueueExitWait", "bb.event.LogExecTTY", "logging.LogRecord", + "bb.build.TaskFailed", "bb.build.TaskBase", "bb.event.ParseStarted", + "bb.event.ParseProgress", "bb.event.ParseCompleted", "bb.event.CacheLoadStarted", + "bb.event.CacheLoadProgress", "bb.event.CacheLoadCompleted", "bb.command.CommandFailed", + "bb.command.CommandExit", "bb.command.CommandCompleted", "bb.cooker.CookerExit", + "bb.event.MultipleProviders", "bb.event.NoProvider", "bb.runqueue.sceneQueueTaskStarted", + "bb.runqueue.runQueueTaskStarted", "bb.runqueue.runQueueTaskFailed", "bb.runqueue.sceneQueueTaskFailed", + "bb.event.BuildBase", "bb.build.TaskStarted", "bb.build.TaskSucceeded", "bb.build.TaskFailedSilent"] + +def main(server, eventHandler, params, tf = TerminalFilter): + + includelogs, loglines, consolelogfile = _log_settings_from_server(server) + + if sys.stdin.isatty() and sys.stdout.isatty(): + log_exec_tty = True + else: + log_exec_tty = False + + helper = uihelper.BBUIHelper() + + console = logging.StreamHandler(sys.stdout) + errconsole = logging.StreamHandler(sys.stderr) + format_str = "%(levelname)s: %(message)s" + format = bb.msg.BBLogFormatter(format_str) + bb.msg.addDefaultlogFilter(console, bb.msg.BBLogFilterStdOut) + bb.msg.addDefaultlogFilter(errconsole, bb.msg.BBLogFilterStdErr) + console.setFormatter(format) + errconsole.setFormatter(format) + logger.addHandler(console) + logger.addHandler(errconsole) + + if params.options.remote_server and params.options.kill_server: + server.terminateServer() + return + + if consolelogfile and not params.options.show_environment: + bb.utils.mkdirhier(os.path.dirname(consolelogfile)) + conlogformat = bb.msg.BBLogFormatter(format_str) + consolelog = logging.FileHandler(consolelogfile) + bb.msg.addDefaultlogFilter(consolelog) + consolelog.setFormatter(conlogformat) + logger.addHandler(consolelog) + + llevel, debug_domains = bb.msg.constructLogOptions() + server.runCommand(["setEventMask", server.getEventHandle(), llevel, debug_domains, _evt_list]) + + if not params.observe_only: + params.updateFromServer(server) + cmdline = params.parseActions() + if not cmdline: + print("Nothing to do. Use 'bitbake world' to build everything, or run 'bitbake --help' for usage information.") + return 1 + if 'msg' in cmdline and cmdline['msg']: + logger.error(cmdline['msg']) + return 1 + + ret, error = server.runCommand(cmdline['action']) + if error: + logger.error("Command '%s' failed: %s" % (cmdline, error)) + return 1 + elif ret != True: + logger.error("Command '%s' failed: returned %s" % (cmdline, ret)) + return 1 + + + parseprogress = None + cacheprogress = None + main.shutdown = 0 + interrupted = False + return_value = 0 + errors = 0 + warnings = 0 + taskfailures = [] + + termfilter = tf(main, helper, console, errconsole, format) + atexit.register(termfilter.finish) + + while True: + try: + event = eventHandler.waitEvent(0) + if event is None: + if main.shutdown > 1: + break + termfilter.updateFooter() + event = eventHandler.waitEvent(0.25) + if event is None: + continue + helper.eventHandler(event) + if isinstance(event, bb.runqueue.runQueueExitWait): + if not main.shutdown: + main.shutdown = 1 + continue + if isinstance(event, bb.event.LogExecTTY): + if log_exec_tty: + tries = event.retries + while tries: + print("Trying to run: %s" % event.prog) + if os.system(event.prog) == 0: + break + time.sleep(event.sleep_delay) + tries -= 1 + if tries: + continue + logger.warn(event.msg) + continue + + if isinstance(event, logging.LogRecord): + if event.levelno >= format.ERROR: + errors = errors + 1 + return_value = 1 + elif event.levelno == format.WARNING: + warnings = warnings + 1 + # For "normal" logging conditions, don't show note logs from tasks + # but do show them if the user has changed the default log level to + # include verbose/debug messages + if event.taskpid != 0 and event.levelno <= format.NOTE: + continue + logger.handle(event) + continue + + if isinstance(event, bb.build.TaskFailedSilent): + logger.warn("Logfile for failed setscene task is %s" % event.logfile) + continue + if isinstance(event, bb.build.TaskFailed): + return_value = 1 + logfile = event.logfile + if logfile and os.path.exists(logfile): + termfilter.clearFooter() + bb.error("Logfile of failure stored in: %s" % logfile) + if includelogs and not event.errprinted: + print("Log data follows:") + f = open(logfile, "r") + lines = [] + while True: + l = f.readline() + if l == '': + break + l = l.rstrip() + if loglines: + lines.append(' | %s' % l) + if len(lines) > int(loglines): + lines.pop(0) + else: + print('| %s' % l) + f.close() + if lines: + for line in lines: + print(line) + if isinstance(event, bb.build.TaskBase): + logger.info(event._message) + continue + if isinstance(event, bb.event.ParseStarted): + if event.total == 0: + continue + parseprogress = new_progress("Parsing recipes", event.total).start() + continue + if isinstance(event, bb.event.ParseProgress): + parseprogress.update(event.current) + continue + if isinstance(event, bb.event.ParseCompleted): + if not parseprogress: + continue + + parseprogress.finish() + print(("Parsing of %d .bb files complete (%d cached, %d parsed). %d targets, %d skipped, %d masked, %d errors." + % ( event.total, event.cached, event.parsed, event.virtuals, event.skipped, event.masked, event.errors))) + continue + + if isinstance(event, bb.event.CacheLoadStarted): + cacheprogress = new_progress("Loading cache", event.total).start() + continue + if isinstance(event, bb.event.CacheLoadProgress): + cacheprogress.update(event.current) + continue + if isinstance(event, bb.event.CacheLoadCompleted): + cacheprogress.finish() + print("Loaded %d entries from dependency cache." % event.num_entries) + continue + + if isinstance(event, bb.command.CommandFailed): + return_value = event.exitcode + if event.error: + errors = errors + 1 + logger.error("Command execution failed: %s", event.error) + main.shutdown = 2 + continue + if isinstance(event, bb.command.CommandExit): + if not return_value: + return_value = event.exitcode + continue + if isinstance(event, (bb.command.CommandCompleted, bb.cooker.CookerExit)): + main.shutdown = 2 + continue + if isinstance(event, bb.event.MultipleProviders): + logger.info("multiple providers are available for %s%s (%s)", event._is_runtime and "runtime " or "", + event._item, + ", ".join(event._candidates)) + logger.info("consider defining a PREFERRED_PROVIDER entry to match %s", event._item) + continue + if isinstance(event, bb.event.NoProvider): + return_value = 1 + errors = errors + 1 + if event._runtime: + r = "R" + else: + r = "" + + extra = '' + if not event._reasons: + if event._close_matches: + extra = ". Close matches:\n %s" % '\n '.join(event._close_matches) + + if event._dependees: + logger.error("Nothing %sPROVIDES '%s' (but %s %sDEPENDS on or otherwise requires it)%s", r, event._item, ", ".join(event._dependees), r, extra) + else: + logger.error("Nothing %sPROVIDES '%s'%s", r, event._item, extra) + if event._reasons: + for reason in event._reasons: + logger.error("%s", reason) + continue + + if isinstance(event, bb.runqueue.sceneQueueTaskStarted): + logger.info("Running setscene task %d of %d (%s)" % (event.stats.completed + event.stats.active + event.stats.failed + 1, event.stats.total, event.taskstring)) + continue + + if isinstance(event, bb.runqueue.runQueueTaskStarted): + if event.noexec: + tasktype = 'noexec task' + else: + tasktype = 'task' + logger.info("Running %s %s of %s (ID: %s, %s)", + tasktype, + event.stats.completed + event.stats.active + + event.stats.failed + 1, + event.stats.total, event.taskid, event.taskstring) + continue + + if isinstance(event, bb.runqueue.runQueueTaskFailed): + taskfailures.append(event.taskstring) + logger.error("Task %s (%s) failed with exit code '%s'", + event.taskid, event.taskstring, event.exitcode) + continue + + if isinstance(event, bb.runqueue.sceneQueueTaskFailed): + logger.warn("Setscene task %s (%s) failed with exit code '%s' - real task will be run instead", + event.taskid, event.taskstring, event.exitcode) + continue + + if isinstance(event, bb.event.DepTreeGenerated): + continue + + # ignore + if isinstance(event, (bb.event.BuildBase, + bb.event.MetadataEvent, + bb.event.StampUpdate, + bb.event.ConfigParsed, + bb.event.RecipeParsed, + bb.event.RecipePreFinalise, + bb.runqueue.runQueueEvent, + bb.event.OperationStarted, + bb.event.OperationCompleted, + bb.event.OperationProgress, + bb.event.DiskFull)): + continue + + logger.error("Unknown event: %s", event) + + except EnvironmentError as ioerror: + termfilter.clearFooter() + # ignore interrupted io + if ioerror.args[0] == 4: + pass + except KeyboardInterrupt: + termfilter.clearFooter() + if params.observe_only: + print("\nKeyboard Interrupt, exiting observer...") + main.shutdown = 2 + if not params.observe_only and main.shutdown == 1: + print("\nSecond Keyboard Interrupt, stopping...\n") + _, error = server.runCommand(["stateForceShutdown"]) + if error: + logger.error("Unable to cleanly stop: %s" % error) + if not params.observe_only and main.shutdown == 0: + print("\nKeyboard Interrupt, closing down...\n") + interrupted = True + _, error = server.runCommand(["stateShutdown"]) + if error: + logger.error("Unable to cleanly shutdown: %s" % error) + main.shutdown = main.shutdown + 1 + pass + + summary = "" + if taskfailures: + summary += pluralise("\nSummary: %s task failed:", + "\nSummary: %s tasks failed:", len(taskfailures)) + for failure in taskfailures: + summary += "\n %s" % failure + if warnings: + summary += pluralise("\nSummary: There was %s WARNING message shown.", + "\nSummary: There were %s WARNING messages shown.", warnings) + if return_value and errors: + summary += pluralise("\nSummary: There was %s ERROR message shown, returning a non-zero exit code.", + "\nSummary: There were %s ERROR messages shown, returning a non-zero exit code.", errors) + if summary: + print(summary) + + if interrupted: + print("Execution was interrupted, returning a non-zero exit code.") + if return_value == 0: + return_value = 1 + + return return_value diff --git a/bitbake/lib/bb/ui/ncurses.py b/bitbake/lib/bb/ui/ncurses.py new file mode 100644 index 0000000000..b6c20ec388 --- /dev/null +++ b/bitbake/lib/bb/ui/ncurses.py @@ -0,0 +1,373 @@ +# +# BitBake Curses UI Implementation +# +# Implements an ncurses frontend for the BitBake utility. +# +# Copyright (C) 2006 Michael 'Mickey' Lauer +# Copyright (C) 2006-2007 Richard Purdie +# +# 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. + +""" + We have the following windows: + + 1.) Main Window: Shows what we are ultimately building and how far we are. Includes status bar + 2.) Thread Activity Window: Shows one status line for every concurrent bitbake thread. + 3.) Command Line Window: Contains an interactive command line where you can interact w/ Bitbake. + + Basic window layout is like that: + + |---------------------------------------------------------| + |
| | + | | 0: foo do_compile complete| + | Building Gtk+-2.6.10 | 1: bar do_patch complete | + | Status: 60% | ... | + | | ... | + | | ... | + |---------------------------------------------------------| + | | + |>>> which virtual/kernel | + |openzaurus-kernel | + |>>> _ | + |---------------------------------------------------------| + +""" + + +from __future__ import division +import logging +import os, sys, itertools, time, subprocess + +try: + import curses +except ImportError: + sys.exit("FATAL: The ncurses ui could not load the required curses python module.") + +import bb +import xmlrpclib +from bb import ui +from bb.ui import uihelper + +parsespin = itertools.cycle( r'|/-\\' ) + +X = 0 +Y = 1 +WIDTH = 2 +HEIGHT = 3 + +MAXSTATUSLENGTH = 32 + +class NCursesUI: + """ + NCurses UI Class + """ + class Window: + """Base Window Class""" + def __init__( self, x, y, width, height, fg=curses.COLOR_BLACK, bg=curses.COLOR_WHITE ): + self.win = curses.newwin( height, width, y, x ) + self.dimensions = ( x, y, width, height ) + """ + if curses.has_colors(): + color = 1 + curses.init_pair( color, fg, bg ) + self.win.bkgdset( ord(' '), curses.color_pair(color) ) + else: + self.win.bkgdset( ord(' '), curses.A_BOLD ) + """ + self.erase() + self.setScrolling() + self.win.noutrefresh() + + def erase( self ): + self.win.erase() + + def setScrolling( self, b = True ): + self.win.scrollok( b ) + self.win.idlok( b ) + + def setBoxed( self ): + self.boxed = True + self.win.box() + self.win.noutrefresh() + + def setText( self, x, y, text, *args ): + self.win.addstr( y, x, text, *args ) + self.win.noutrefresh() + + def appendText( self, text, *args ): + self.win.addstr( text, *args ) + self.win.noutrefresh() + + def drawHline( self, y ): + self.win.hline( y, 0, curses.ACS_HLINE, self.dimensions[WIDTH] ) + self.win.noutrefresh() + + class DecoratedWindow( Window ): + """Base class for windows with a box and a title bar""" + def __init__( self, title, x, y, width, height, fg=curses.COLOR_BLACK, bg=curses.COLOR_WHITE ): + NCursesUI.Window.__init__( self, x+1, y+3, width-2, height-4, fg, bg ) + self.decoration = NCursesUI.Window( x, y, width, height, fg, bg ) + self.decoration.setBoxed() + self.decoration.win.hline( 2, 1, curses.ACS_HLINE, width-2 ) + self.setTitle( title ) + + def setTitle( self, title ): + self.decoration.setText( 1, 1, title.center( self.dimensions[WIDTH]-2 ), curses.A_BOLD ) + + #-------------------------------------------------------------------------# +# class TitleWindow( Window ): + #-------------------------------------------------------------------------# +# """Title Window""" +# def __init__( self, x, y, width, height ): +# NCursesUI.Window.__init__( self, x, y, width, height ) +# version = bb.__version__ +# title = "BitBake %s" % version +# credit = "(C) 2003-2007 Team BitBake" +# #self.win.hline( 2, 1, curses.ACS_HLINE, width-2 ) +# self.win.border() +# self.setText( 1, 1, title.center( self.dimensions[WIDTH]-2 ), curses.A_BOLD ) +# self.setText( 1, 2, credit.center( self.dimensions[WIDTH]-2 ), curses.A_BOLD ) + + #-------------------------------------------------------------------------# + class ThreadActivityWindow( DecoratedWindow ): + #-------------------------------------------------------------------------# + """Thread Activity Window""" + def __init__( self, x, y, width, height ): + NCursesUI.DecoratedWindow.__init__( self, "Thread Activity", x, y, width, height ) + + def setStatus( self, thread, text ): + line = "%02d: %s" % ( thread, text ) + width = self.dimensions[WIDTH] + if ( len(line) > width ): + line = line[:width-3] + "..." + else: + line = line.ljust( width ) + self.setText( 0, thread, line ) + + #-------------------------------------------------------------------------# + class MainWindow( DecoratedWindow ): + #-------------------------------------------------------------------------# + """Main Window""" + def __init__( self, x, y, width, height ): + self.StatusPosition = width - MAXSTATUSLENGTH + NCursesUI.DecoratedWindow.__init__( self, None, x, y, width, height ) + curses.nl() + + def setTitle( self, title ): + title = "BitBake %s" % bb.__version__ + self.decoration.setText( 2, 1, title, curses.A_BOLD ) + self.decoration.setText( self.StatusPosition - 8, 1, "Status:", curses.A_BOLD ) + + def setStatus(self, status): + while len(status) < MAXSTATUSLENGTH: + status = status + " " + self.decoration.setText( self.StatusPosition, 1, status, curses.A_BOLD ) + + + #-------------------------------------------------------------------------# + class ShellOutputWindow( DecoratedWindow ): + #-------------------------------------------------------------------------# + """Interactive Command Line Output""" + def __init__( self, x, y, width, height ): + NCursesUI.DecoratedWindow.__init__( self, "Command Line Window", x, y, width, height ) + + #-------------------------------------------------------------------------# + class ShellInputWindow( Window ): + #-------------------------------------------------------------------------# + """Interactive Command Line Input""" + def __init__( self, x, y, width, height ): + NCursesUI.Window.__init__( self, x, y, width, height ) + +# put that to the top again from curses.textpad import Textbox +# self.textbox = Textbox( self.win ) +# t = threading.Thread() +# t.run = self.textbox.edit +# t.start() + + #-------------------------------------------------------------------------# + def main(self, stdscr, server, eventHandler, params): + #-------------------------------------------------------------------------# + height, width = stdscr.getmaxyx() + + # for now split it like that: + # MAIN_y + THREAD_y = 2/3 screen at the top + # MAIN_x = 2/3 left, THREAD_y = 1/3 right + # CLI_y = 1/3 of screen at the bottom + # CLI_x = full + + main_left = 0 + main_top = 0 + main_height = ( height // 3 * 2 ) + main_width = ( width // 3 ) * 2 + clo_left = main_left + clo_top = main_top + main_height + clo_height = height - main_height - main_top - 1 + clo_width = width + cli_left = main_left + cli_top = clo_top + clo_height + cli_height = 1 + cli_width = width + thread_left = main_left + main_width + thread_top = main_top + thread_height = main_height + thread_width = width - main_width + + #tw = self.TitleWindow( 0, 0, width, main_top ) + mw = self.MainWindow( main_left, main_top, main_width, main_height ) + taw = self.ThreadActivityWindow( thread_left, thread_top, thread_width, thread_height ) + clo = self.ShellOutputWindow( clo_left, clo_top, clo_width, clo_height ) + cli = self.ShellInputWindow( cli_left, cli_top, cli_width, cli_height ) + cli.setText( 0, 0, "BB>" ) + + mw.setStatus("Idle") + + helper = uihelper.BBUIHelper() + shutdown = 0 + + try: + params.updateFromServer(server) + cmdline = params.parseActions() + if not cmdline: + print("Nothing to do. Use 'bitbake world' to build everything, or run 'bitbake --help' for usage information.") + return 1 + if 'msg' in cmdline and cmdline['msg']: + logger.error(cmdline['msg']) + return 1 + cmdline = cmdline['action'] + ret, error = server.runCommand(cmdline) + if error: + print("Error running command '%s': %s" % (cmdline, error)) + return + elif ret != True: + print("Couldn't get default commandlind! %s" % ret) + return + except xmlrpclib.Fault as x: + print("XMLRPC Fault getting commandline:\n %s" % x) + return + + exitflag = False + while not exitflag: + try: + event = eventHandler.waitEvent(0.25) + if not event: + continue + + helper.eventHandler(event) + if isinstance(event, bb.build.TaskBase): + mw.appendText("NOTE: %s\n" % event._message) + if isinstance(event, logging.LogRecord): + mw.appendText(logging.getLevelName(event.levelno) + ': ' + event.getMessage() + '\n') + + if isinstance(event, bb.event.CacheLoadStarted): + self.parse_total = event.total + if isinstance(event, bb.event.CacheLoadProgress): + x = event.current + y = self.parse_total + mw.setStatus("Loading Cache: %s [%2d %%]" % ( next(parsespin), x*100/y ) ) + if isinstance(event, bb.event.CacheLoadCompleted): + mw.setStatus("Idle") + mw.appendText("Loaded %d entries from dependency cache.\n" + % ( event.num_entries)) + + if isinstance(event, bb.event.ParseStarted): + self.parse_total = event.total + if isinstance(event, bb.event.ParseProgress): + x = event.current + y = self.parse_total + mw.setStatus("Parsing Recipes: %s [%2d %%]" % ( next(parsespin), x*100/y ) ) + if isinstance(event, bb.event.ParseCompleted): + mw.setStatus("Idle") + mw.appendText("Parsing finished. %d cached, %d parsed, %d skipped, %d masked.\n" + % ( event.cached, event.parsed, event.skipped, event.masked )) + +# if isinstance(event, bb.build.TaskFailed): +# if event.logfile: +# if data.getVar("BBINCLUDELOGS", d): +# bb.error("log data follows (%s)" % logfile) +# number_of_lines = data.getVar("BBINCLUDELOGS_LINES", d) +# if number_of_lines: +# subprocess.call('tail -n%s %s' % (number_of_lines, logfile), shell=True) +# else: +# f = open(logfile, "r") +# while True: +# l = f.readline() +# if l == '': +# break +# l = l.rstrip() +# print '| %s' % l +# f.close() +# else: +# bb.error("see log in %s" % logfile) + + if isinstance(event, bb.command.CommandCompleted): + # stop so the user can see the result of the build, but + # also allow them to now exit with a single ^C + shutdown = 2 + if isinstance(event, bb.command.CommandFailed): + mw.appendText("Command execution failed: %s" % event.error) + time.sleep(2) + exitflag = True + if isinstance(event, bb.command.CommandExit): + exitflag = True + if isinstance(event, bb.cooker.CookerExit): + exitflag = True + + if isinstance(event, bb.event.LogExecTTY): + mw.appendText('WARN: ' + event.msg + '\n') + if helper.needUpdate: + activetasks, failedtasks = helper.getTasks() + taw.erase() + taw.setText(0, 0, "") + if activetasks: + taw.appendText("Active Tasks:\n") + for task in activetasks.itervalues(): + taw.appendText(task["title"] + '\n') + if failedtasks: + taw.appendText("Failed Tasks:\n") + for task in failedtasks: + taw.appendText(task["title"] + '\n') + + curses.doupdate() + except EnvironmentError as ioerror: + # ignore interrupted io + if ioerror.args[0] == 4: + pass + + except KeyboardInterrupt: + if shutdown == 2: + mw.appendText("Third Keyboard Interrupt, exit.\n") + exitflag = True + if shutdown == 1: + mw.appendText("Second Keyboard Interrupt, stopping...\n") + _, error = server.runCommand(["stateForceShutdown"]) + if error: + print("Unable to cleanly stop: %s" % error) + if shutdown == 0: + mw.appendText("Keyboard Interrupt, closing down...\n") + _, error = server.runCommand(["stateShutdown"]) + if error: + print("Unable to cleanly shutdown: %s" % error) + shutdown = shutdown + 1 + pass + +def main(server, eventHandler): + if not os.isatty(sys.stdout.fileno()): + print("FATAL: Unable to run 'ncurses' UI without a TTY.") + return + ui = NCursesUI() + try: + curses.wrapper(ui.main, server, eventHandler) + except: + import traceback + traceback.print_exc() diff --git a/bitbake/lib/bb/ui/puccho.py b/bitbake/lib/bb/ui/puccho.py new file mode 100644 index 0000000000..3ce4590c16 --- /dev/null +++ b/bitbake/lib/bb/ui/puccho.py @@ -0,0 +1,425 @@ +# +# BitBake Graphical GTK User Interface +# +# Copyright (C) 2008 Intel Corporation +# +# Authored by Rob Bradford +# +# 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.... + # ;|...;|...;|... + 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 ( + """Enter the repository location and press _Refresh""") + + 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() diff --git a/bitbake/lib/bb/ui/toasterui.py b/bitbake/lib/bb/ui/toasterui.py new file mode 100644 index 0000000000..9a9fe6f2d8 --- /dev/null +++ b/bitbake/lib/bb/ui/toasterui.py @@ -0,0 +1,292 @@ +# +# BitBake ToasterUI Implementation +# based on (No)TTY UI Implementation by Richard Purdie +# +# Handling output to TTYs or files (no TTY) +# +# Copyright (C) 2006-2012 Richard Purdie +# Copyright (C) 2013 Intel Corporation +# +# 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. + +from __future__ import division +try: + import bb +except RuntimeError as exc: + sys.exit(str(exc)) + +from bb.ui import uihelper +from bb.ui.buildinfohelper import BuildInfoHelper + +import bb.msg +import copy +import fcntl +import logging +import os +import progressbar +import signal +import struct +import sys +import time +import xmlrpclib + +featureSet = [bb.cooker.CookerFeatures.HOB_EXTRA_CACHES, bb.cooker.CookerFeatures.SEND_DEPENDS_TREE, bb.cooker.CookerFeatures.BASEDATASTORE_TRACKING] + +logger = logging.getLogger("BitBake") +interactive = sys.stdout.isatty() + + + +def _log_settings_from_server(server): + # Get values of variables which control our output + includelogs, error = server.runCommand(["getVariable", "BBINCLUDELOGS"]) + if error: + logger.error("Unable to get the value of BBINCLUDELOGS variable: %s" % error) + raise BaseException(error) + loglines, error = server.runCommand(["getVariable", "BBINCLUDELOGS_LINES"]) + if error: + logger.error("Unable to get the value of BBINCLUDELOGS_LINES variable: %s" % error) + raise BaseException(error) + return includelogs, loglines + +def main(server, eventHandler, params ): + + includelogs, loglines = _log_settings_from_server(server) + + # verify and warn + build_history_enabled = True + inheritlist, error = server.runCommand(["getVariable", "INHERIT"]) + if not "buildhistory" in inheritlist.split(" "): + logger.warn("buildhistory is not enabled. Please enable INHERIT += \"buildhistory\" to see image details.") + build_history_enabled = False + + helper = uihelper.BBUIHelper() + + console = logging.StreamHandler(sys.stdout) + format_str = "%(levelname)s: %(message)s" + format = bb.msg.BBLogFormatter(format_str) + bb.msg.addDefaultlogFilter(console) + console.setFormatter(format) + logger.addHandler(console) + + if not params.observe_only: + logger.error("ToasterUI can only work in observer mode") + return + + + main.shutdown = 0 + interrupted = False + return_value = 0 + errors = 0 + warnings = 0 + taskfailures = [] + + buildinfohelper = BuildInfoHelper(server, build_history_enabled) + + + while True: + try: + event = eventHandler.waitEvent(0.25) + + if event is None: + if main.shutdown > 0: + break + continue + + helper.eventHandler(event) + + if isinstance(event, bb.event.BuildStarted): + buildinfohelper.store_started_build(event) + + if isinstance(event, (bb.build.TaskStarted, bb.build.TaskSucceeded, bb.build.TaskFailedSilent)): + buildinfohelper.update_and_store_task(event) + continue + + if isinstance(event, bb.event.LogExecTTY): + logger.warn(event.msg) + continue + + if isinstance(event, logging.LogRecord): + buildinfohelper.store_log_event(event) + if event.levelno >= format.ERROR: + errors = errors + 1 + return_value = 1 + elif event.levelno == format.WARNING: + warnings = warnings + 1 + # For "normal" logging conditions, don't show note logs from tasks + # but do show them if the user has changed the default log level to + # include verbose/debug messages + if event.taskpid != 0 and event.levelno <= format.NOTE: + continue + + logger.handle(event) + continue + + if isinstance(event, bb.build.TaskFailed): + buildinfohelper.update_and_store_task(event) + return_value = 1 + logfile = event.logfile + if logfile and os.path.exists(logfile): + bb.error("Logfile of failure stored in: %s" % logfile) + continue + + # these events are unprocessed now, but may be used in the future to log + # timing and error informations from the parsing phase in Toaster + if isinstance(event, (bb.event.SanityCheckPassed, bb.event.SanityCheck)): + continue + if isinstance(event, bb.event.ParseStarted): + continue + if isinstance(event, bb.event.ParseProgress): + continue + if isinstance(event, bb.event.ParseCompleted): + continue + if isinstance(event, bb.event.CacheLoadStarted): + continue + if isinstance(event, bb.event.CacheLoadProgress): + continue + if isinstance(event, bb.event.CacheLoadCompleted): + continue + if isinstance(event, bb.event.MultipleProviders): + continue + if isinstance(event, bb.event.NoProvider): + return_value = 1 + errors = errors + 1 + if event._runtime: + r = "R" + else: + r = "" + + if event._dependees: + text = "Nothing %sPROVIDES '%s' (but %s %sDEPENDS on or otherwise requires it)" % (r, event._item, ", ".join(event._dependees), r) + else: + text = "Nothing %sPROVIDES '%s'" % (r, event._item) + + logger.error(text) + if event._reasons: + for reason in event._reasons: + logger.error("%s", reason) + text += reason + buildinfohelper.store_log_error(text) + continue + + if isinstance(event, bb.event.ConfigParsed): + continue + if isinstance(event, bb.event.RecipeParsed): + continue + + # end of saved events + + if isinstance(event, (bb.runqueue.sceneQueueTaskStarted, bb.runqueue.runQueueTaskStarted, bb.runqueue.runQueueTaskSkipped)): + buildinfohelper.store_started_task(event) + continue + + if isinstance(event, bb.runqueue.runQueueTaskCompleted): + buildinfohelper.update_and_store_task(event) + continue + + if isinstance(event, bb.runqueue.runQueueTaskFailed): + buildinfohelper.update_and_store_task(event) + taskfailures.append(event.taskstring) + logger.error("Task %s (%s) failed with exit code '%s'", + event.taskid, event.taskstring, event.exitcode) + continue + + if isinstance(event, (bb.runqueue.sceneQueueTaskCompleted, bb.runqueue.sceneQueueTaskFailed)): + buildinfohelper.update_and_store_task(event) + continue + + + if isinstance(event, (bb.event.TreeDataPreparationStarted, bb.event.TreeDataPreparationCompleted)): + continue + + if isinstance(event, (bb.event.BuildCompleted)): + continue + + if isinstance(event, (bb.command.CommandCompleted, + bb.command.CommandFailed, + bb.command.CommandExit)): + if (isinstance(event, bb.command.CommandFailed)): + event.levelno = format.ERROR + event.msg = event.error + event.pathname = "" + event.lineno = 0 + buildinfohelper.store_log_event(event) + errors += 1 + + buildinfohelper.update_build_information(event, errors, warnings, taskfailures) + + # we start a new build info + errors = 0 + warnings = 0 + taskfailures = [] + buildinfohelper = BuildInfoHelper(server, build_history_enabled) + continue + + if isinstance(event, bb.event.MetadataEvent): + if event.type == "SinglePackageInfo": + buildinfohelper.store_build_package_information(event) + elif event.type == "LayerInfo": + buildinfohelper.store_layer_info(event) + elif event.type == "BuildStatsList": + buildinfohelper.store_tasks_stats(event) + elif event.type == "ImagePkgList": + buildinfohelper.store_target_package_data(event) + elif event.type == "MissedSstate": + buildinfohelper.store_missed_state_tasks(event) + elif event.type == "ImageFileSize": + buildinfohelper.update_target_image_file(event) + elif event.type == "LicenseManifestPath": + buildinfohelper.store_license_manifest_path(event) + continue + + if isinstance(event, bb.cooker.CookerExit): + # exit when the server exits + break + + # ignore + if isinstance(event, (bb.event.BuildBase, + bb.event.StampUpdate, + bb.event.RecipePreFinalise, + bb.runqueue.runQueueEvent, + bb.runqueue.runQueueExitWait, + bb.event.OperationProgress, + bb.command.CommandFailed, + bb.command.CommandExit, + bb.command.CommandCompleted)): + continue + + if isinstance(event, bb.event.DepTreeGenerated): + buildinfohelper.store_dependency_information(event) + continue + + logger.error("Unknown event: %s", event) + + except EnvironmentError as ioerror: + # ignore interrupted io + if ioerror.args[0] == 4: + pass + except KeyboardInterrupt: + main.shutdown = 1 + pass + except Exception as e: + logger.error(e) + import traceback + traceback.print_exc() + pass + + if interrupted: + if return_value == 0: + return_value = 1 + + return return_value diff --git a/bitbake/lib/bb/ui/uievent.py b/bitbake/lib/bb/ui/uievent.py new file mode 100644 index 0000000000..98658f68bf --- /dev/null +++ b/bitbake/lib/bb/ui/uievent.py @@ -0,0 +1,133 @@ +# ex:ts=4:sw=4:sts=4:et +# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- +# +# Copyright (C) 2006 - 2007 Michael 'Mickey' Lauer +# Copyright (C) 2006 - 2007 Richard Purdie +# +# 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. + + +""" +Use this class to fork off a thread to recieve event callbacks from the bitbake +server and queue them for the UI to process. This process must be used to avoid +client/server deadlocks. +""" + +import socket, threading, pickle +from SimpleXMLRPCServer import SimpleXMLRPCServer, SimpleXMLRPCRequestHandler + +class BBUIEventQueue: + def __init__(self, BBServer, clientinfo=("localhost, 0")): + + self.eventQueue = [] + self.eventQueueLock = threading.Lock() + self.eventQueueNotify = threading.Event() + + self.BBServer = BBServer + self.clientinfo = clientinfo + + server = UIXMLRPCServer(self.clientinfo) + self.host, self.port = server.socket.getsockname() + + server.register_function( self.system_quit, "event.quit" ) + server.register_function( self.send_event, "event.sendpickle" ) + server.socket.settimeout(1) + + self.EventHandle = self.BBServer.registerEventHandler(self.host, self.port) + + if (self.EventHandle == None): + bb.fatal("Could not register UI event handler") + + self.server = server + + self.t = threading.Thread() + self.t.setDaemon(True) + self.t.run = self.startCallbackHandler + self.t.start() + + def getEvent(self): + + self.eventQueueLock.acquire() + + if len(self.eventQueue) == 0: + self.eventQueueLock.release() + return None + + item = self.eventQueue.pop(0) + + if len(self.eventQueue) == 0: + self.eventQueueNotify.clear() + + self.eventQueueLock.release() + return item + + def waitEvent(self, delay): + self.eventQueueNotify.wait(delay) + return self.getEvent() + + def queue_event(self, event): + self.eventQueueLock.acquire() + self.eventQueue.append(event) + self.eventQueueNotify.set() + self.eventQueueLock.release() + + def send_event(self, event): + self.queue_event(pickle.loads(event)) + + def startCallbackHandler(self): + + self.server.timeout = 1 + while not self.server.quit: + self.server.handle_request() + self.server.server_close() + + def system_quit( self ): + """ + Shut down the callback thread + """ + try: + self.BBServer.unregisterEventHandler(self.EventHandle) + except: + pass + self.server.quit = True + +class UIXMLRPCServer (SimpleXMLRPCServer): + + def __init__( self, interface ): + self.quit = False + SimpleXMLRPCServer.__init__( self, + interface, + requestHandler=SimpleXMLRPCRequestHandler, + logRequests=False, allow_none=True) + + def get_request(self): + while not self.quit: + try: + sock, addr = self.socket.accept() + sock.settimeout(1) + return (sock, addr) + except socket.timeout: + pass + return (None, None) + + def close_request(self, request): + if request is None: + return + SimpleXMLRPCServer.close_request(self, request) + + def process_request(self, request, client_address): + if request is None: + return + SimpleXMLRPCServer.process_request(self, request, client_address) + diff --git a/bitbake/lib/bb/ui/uihelper.py b/bitbake/lib/bb/ui/uihelper.py new file mode 100644 index 0000000000..a703387fb8 --- /dev/null +++ b/bitbake/lib/bb/ui/uihelper.py @@ -0,0 +1,100 @@ +# ex:ts=4:sw=4:sts=4:et +# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- +# +# Copyright (C) 2006 - 2007 Michael 'Mickey' Lauer +# Copyright (C) 2006 - 2007 Richard Purdie +# +# 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 bb.build + +class BBUIHelper: + def __init__(self): + self.needUpdate = False + self.running_tasks = {} + # Running PIDs preserves the order tasks were executed in + self.running_pids = [] + self.failed_tasks = [] + self.tasknumber_current = 0 + self.tasknumber_total = 0 + + def eventHandler(self, event): + if isinstance(event, bb.build.TaskStarted): + self.running_tasks[event.pid] = { 'title' : "%s %s" % (event._package, event._task) } + self.running_pids.append(event.pid) + self.needUpdate = True + if isinstance(event, bb.build.TaskSucceeded): + del self.running_tasks[event.pid] + self.running_pids.remove(event.pid) + self.needUpdate = True + if isinstance(event, bb.build.TaskFailedSilent): + del self.running_tasks[event.pid] + self.running_pids.remove(event.pid) + # Don't add to the failed tasks list since this is e.g. a setscene task failure + self.needUpdate = True + if isinstance(event, bb.build.TaskFailed): + del self.running_tasks[event.pid] + self.running_pids.remove(event.pid) + self.failed_tasks.append( { 'title' : "%s %s" % (event._package, event._task)}) + self.needUpdate = True + if isinstance(event, bb.runqueue.runQueueTaskStarted) or isinstance(event, bb.runqueue.sceneQueueTaskStarted): + self.tasknumber_current = event.stats.completed + event.stats.active + event.stats.failed + 1 + self.tasknumber_total = event.stats.total + self.needUpdate = True + + def getTasks(self): + self.needUpdate = False + return (self.running_tasks, self.failed_tasks) + + def findServerDetails(self): + import sys + import optparse + from bb.server.xmlrpc import BitbakeServerInfo, BitBakeServerConnection + host = "" + port = 0 + bind = "" + parser = optparse.OptionParser( + usage = """%prog -H host -P port -B bindaddr""") + + parser.add_option("-H", "--host", help = "Bitbake server's IP address", + action = "store", dest = "host", default = None) + + parser.add_option("-P", "--port", help = "Bitbake server's Port number", + action = "store", dest = "port", default = None) + + parser.add_option("-B", "--bind", help = "Hob2 local bind address", + action = "store", dest = "bind", default = None) + + options, args = parser.parse_args(sys.argv) + for key, val in options.__dict__.items(): + if key == 'host' and val: + host = val + elif key == 'port' and val: + port = int(val) + elif key == 'bind' and val: + bind = val + + if not host or not port or not bind: + parser.print_usage() + sys.exit(1) + + serverinfo = BitbakeServerInfo(host, port) + clientinfo = (bind, 0) + connection = BitBakeServerConnection(serverinfo, clientinfo) + + server = connection.connection + eventHandler = connection.events + + return server, eventHandler, host, bind + -- cgit v1.2.3-54-g00ecf