diff options
Diffstat (limited to 'yocto-poky/bitbake/lib/toaster/orm/models.py')
-rw-r--r-- | yocto-poky/bitbake/lib/toaster/orm/models.py | 560 |
1 files changed, 484 insertions, 76 deletions
diff --git a/yocto-poky/bitbake/lib/toaster/orm/models.py b/yocto-poky/bitbake/lib/toaster/orm/models.py index 383290583..0b83b991b 100644 --- a/yocto-poky/bitbake/lib/toaster/orm/models.py +++ b/yocto-poky/bitbake/lib/toaster/orm/models.py @@ -19,9 +19,12 @@ # 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 unicode_literals + from django.db import models, IntegrityError -from django.db.models import F, Q, Avg, Max +from django.db.models import F, Q, Avg, Max, Sum from django.utils import timezone +from django.utils.encoding import force_bytes from django.core.urlresolvers import reverse @@ -29,10 +32,62 @@ from django.core import validators from django.conf import settings import django.db.models.signals +import os.path +import re +import itertools import logging logger = logging.getLogger("toaster") +if 'sqlite' in settings.DATABASES['default']['ENGINE']: + from django.db import transaction, OperationalError + from time import sleep + + _base_save = models.Model.save + def save(self, *args, **kwargs): + while True: + try: + with transaction.atomic(): + return _base_save(self, *args, **kwargs) + except OperationalError as err: + if 'database is locked' in str(err): + logger.warning("%s, model: %s, args: %s, kwargs: %s", + err, self.__class__, args, kwargs) + sleep(0.5) + continue + raise + + models.Model.save = save + + # HACK: Monkey patch Django to fix 'database is locked' issue + + from django.db.models.query import QuerySet + _base_insert = QuerySet._insert + def _insert(self, *args, **kwargs): + with transaction.atomic(using=self.db, savepoint=False): + return _base_insert(self, *args, **kwargs) + QuerySet._insert = _insert + + from django.utils import six + def _create_object_from_params(self, lookup, params): + """ + Tries to create an object using passed params. + Used by get_or_create and update_or_create + """ + try: + obj = self.create(**params) + return obj, True + except IntegrityError: + exc_info = sys.exc_info() + try: + return self.get(**lookup), False + except self.model.DoesNotExist: + pass + six.reraise(*exc_info) + + QuerySet._create_object_from_params = _create_object_from_params + + # end of HACK class GitURLValidator(validators.URLValidator): import re @@ -91,18 +146,25 @@ class ProjectManager(models.Manager): return prj - def create(self, *args, **kwargs): - raise Exception("Invalid call to Project.objects.create. Use Project.objects.create_project() to create a project") - # return single object with is_default = True - def get_default_project(self): + def get_or_create_default_project(self): projects = super(ProjectManager, self).filter(is_default = True) + if len(projects) > 1: - raise Exception("Inconsistent project data: multiple " + - "default projects (i.e. with is_default=True)") + raise Exception('Inconsistent project data: multiple ' + + 'default projects (i.e. with is_default=True)') elif len(projects) < 1: - raise Exception("Inconsistent project data: no default project found") - return projects[0] + options = { + 'name': 'Command line builds', + 'short_description': 'Project for builds started outside Toaster', + 'is_default': True + } + project = Project.objects.create(**options) + project.save() + + return project + else: + return projects[0] class Project(models.Model): search_allowed_fields = ['name', 'short_description', 'release__name', 'release__branch_name'] @@ -130,13 +192,15 @@ class Project(models.Model): try: return self.projectvariable_set.get(name="MACHINE").value except (ProjectVariable.DoesNotExist,IndexError): - return( "None" ); + return None; def get_number_of_builds(self): - try: - return len(Build.objects.filter( project = self.id )) - except (Build.DoesNotExist,IndexError): - return( 0 ) + """Return the number of builds which have ended""" + + return self.build_set.exclude( + Q(outcome=Build.IN_PROGRESS) | + Q(outcome=Build.CANCELLED) + ).count() def get_last_build_id(self): try: @@ -180,6 +244,14 @@ class Project(models.Model): except (Build.DoesNotExist,IndexError): return( "not_found" ) + def get_last_build_extensions(self): + """ + Get list of file name extensions for images produced by the most + recent build + """ + last_build = Build.objects.get(pk = self.get_last_build_id()) + return last_build.get_image_file_extensions() + def get_last_imgfiles(self): build_id = self.get_last_build_id if (-1 == build_id): @@ -189,40 +261,32 @@ class Project(models.Model): except (Variable.DoesNotExist,IndexError): return( "not_found" ) - # returns a queryset of compatible layers for a project - def compatible_layerversions(self, release = None, layer_name = None): - logger.warning("This function is deprecated") - if release == None: - release = self.release - # layers on the same branch or layers specifically set for this project - queryset = Layer_Version.objects.filter(((Q(up_branch__name = release.branch_name) & Q(project = None)) | Q(project = self)) & Q(build__isnull=True)) - - if layer_name is not None: - # we select only a layer name - queryset = queryset.filter(layer__name = layer_name) - - # order by layer version priority - queryset = queryset.filter(Q(layer_source=None) | Q(layer_source__releaselayersourcepriority__release = release)).select_related('layer_source', 'layer', 'up_branch', "layer_source__releaselayersourcepriority__priority").order_by("-layer_source__releaselayersourcepriority__priority") - - return queryset - def get_all_compatible_layer_versions(self): """ Returns Queryset of all Layer_Versions which are compatible with this project""" - queryset = Layer_Version.objects.filter( - (Q(up_branch__name=self.release.branch_name) & Q(build=None)) - | Q(project=self)) + queryset = None + + # guard on release, as it can be null + if self.release: + queryset = Layer_Version.objects.filter( + (Q(up_branch__name=self.release.branch_name) & + Q(build=None) & + Q(project=None)) | + Q(project=self)) + else: + queryset = Layer_Version.objects.none() return queryset def get_project_layer_versions(self, pk=False): """ Returns the Layer_Versions currently added to this project """ - layer_versions = self.projectlayer_set.all().values('layercommit') + layer_versions = self.projectlayer_set.all().values_list('layercommit', + flat=True) if pk is False: - return layer_versions + return Layer_Version.objects.filter(pk__in=layer_versions) else: - return layer_versions.values_list('layercommit__pk', flat=True) + return layer_versions def get_available_machines(self): @@ -303,11 +367,13 @@ class Build(models.Model): SUCCEEDED = 0 FAILED = 1 IN_PROGRESS = 2 + CANCELLED = 3 BUILD_OUTCOME = ( (SUCCEEDED, 'Succeeded'), (FAILED, 'Failed'), (IN_PROGRESS, 'In Progress'), + (CANCELLED, 'Cancelled'), ) search_allowed_fields = ['machine', 'cooker_log_path', "target__target", "target__target_image_file__file_name"] @@ -323,11 +389,40 @@ class Build(models.Model): build_name = models.CharField(max_length=100) bitbake_version = models.CharField(max_length=50) + @staticmethod + def get_recent(project=None): + """ + Return recent builds as a list; if project is set, only return + builds for that project + """ + + builds = Build.objects.all() + + if project: + builds = builds.filter(project=project) + + finished_criteria = \ + Q(outcome=Build.SUCCEEDED) | \ + Q(outcome=Build.FAILED) | \ + Q(outcome=Build.CANCELLED) + + recent_builds = list(itertools.chain( + builds.filter(outcome=Build.IN_PROGRESS).order_by("-started_on"), + builds.filter(finished_criteria).order_by("-completed_on")[:3] + )) + + # add percentage done property to each build; this is used + # to show build progress in mrb_section.html + for build in recent_builds: + build.percentDone = build.completeper() + + return recent_builds + def completeper(self): tf = Task.objects.filter(build = self) tfc = tf.count() if tfc > 0: - completeper = tf.exclude(order__isnull=True).count()*100/tf.count() + completeper = tf.exclude(order__isnull=True).count()*100/tfc else: completeper = 0 return completeper @@ -339,15 +434,117 @@ class Build(models.Model): eta += ((eta - self.started_on)*(100-completeper))/completeper return eta + def get_image_file_extensions(self): + """ + Get list of file name extensions for images produced by this build + """ + targets = Target.objects.filter(build_id = self.id) + extensions = [] + + # pattern to match against file path for building extension string + pattern = re.compile('\.([^\.]+?)$') + + for target in targets: + if (not target.is_image): + continue + + target_image_files = Target_Image_File.objects.filter(target_id = target.id) + + for target_image_file in target_image_files: + file_name = os.path.basename(target_image_file.file_name) + suffix = '' + + continue_matching = True + + # incrementally extract the suffix from the file path, + # checking it against the list of valid suffixes at each + # step; if the path is stripped of all potential suffix + # parts without matching a valid suffix, this returns all + # characters after the first '.' in the file name + while continue_matching: + matches = pattern.search(file_name) + + if None == matches: + continue_matching = False + suffix = re.sub('^\.', '', suffix) + continue + else: + suffix = matches.group(1) + suffix + + if suffix in Target_Image_File.SUFFIXES: + continue_matching = False + continue + else: + # reduce the file name and try to find the next + # segment from the path which might be part + # of the suffix + file_name = re.sub('.' + matches.group(1), '', file_name) + suffix = '.' + suffix + + if not suffix in extensions: + extensions.append(suffix) + + return ', '.join(extensions) def get_sorted_target_list(self): tgts = Target.objects.filter(build_id = self.id).order_by( 'target' ); return( tgts ); + def get_recipes(self): + """ + Get the recipes related to this build; + note that the related layer versions and layers are also prefetched + by this query, as this queryset can be sorted by these objects in the + build recipes view; prefetching them here removes the need + for another query in that view + """ + layer_versions = Layer_Version.objects.filter(build=self) + criteria = Q(layer_version__id__in=layer_versions) + return Recipe.objects.filter(criteria) \ + .select_related('layer_version', 'layer_version__layer') + + def get_image_recipes(self): + """ + Returns a list of image Recipes (custom and built-in) related to this + build, sorted by name; note that this has to be done in two steps, as + there's no way to get all the custom image recipes and image recipes + in one query + """ + custom_image_recipes = self.get_custom_image_recipes() + custom_image_recipe_names = custom_image_recipes.values_list('name', flat=True) + + not_custom_image_recipes = ~Q(name__in=custom_image_recipe_names) & \ + Q(is_image=True) + + built_image_recipes = self.get_recipes().filter(not_custom_image_recipes) + + # append to the custom image recipes and sort + customisable_image_recipes = list( + itertools.chain(custom_image_recipes, built_image_recipes) + ) + + return sorted(customisable_image_recipes, key=lambda recipe: recipe.name) + + def get_custom_image_recipes(self): + """ + Returns a queryset of CustomImageRecipes related to this build, + sorted by name + """ + built_recipe_names = self.get_recipes().values_list('name', flat=True) + criteria = Q(name__in=built_recipe_names) & Q(project=self.project) + queryset = CustomImageRecipe.objects.filter(criteria).order_by('name') + return queryset + def get_outcome_text(self): return Build.BUILD_OUTCOME[int(self.outcome)][1] @property + def failed_tasks(self): + """ Get failed tasks for the build """ + tasks = self.task_build.all() + return tasks.filter(order__gt=0, outcome=Task.OUTCOME_FAILED) + + @property def errors(self): return (self.logmessage_set.filter(level=LogMessage.ERROR) | self.logmessage_set.filter(level=LogMessage.EXCEPTION) | @@ -358,8 +555,26 @@ class Build(models.Model): return self.logmessage_set.filter(level=LogMessage.WARNING) @property + def timespent(self): + return self.completed_on - self.started_on + + @property def timespent_seconds(self): - return (self.completed_on - self.started_on).total_seconds() + return self.timespent.total_seconds() + + @property + def target_labels(self): + """ + Sorted (a-z) "target1:task, target2, target3" etc. string for all + targets in this build + """ + targets = self.target_set.all() + target_labels = [target.target + + (':' + target.task if target.task else '') + for target in targets] + target_labels.sort() + + return target_labels def get_current_status(self): """ @@ -399,6 +614,8 @@ class BuildArtifact(models.Model): return self.file_name + def get_basename(self): + return os.path.basename(self.file_name) def is_available(self): return self.build.buildrequest.environment.has_artifact(self.file_name) @@ -424,10 +641,25 @@ class Target(models.Model): return self.target class Target_Image_File(models.Model): + # valid suffixes for image files produced by a build + SUFFIXES = { + 'btrfs', 'cpio', 'cpio.gz', 'cpio.lz4', 'cpio.lzma', 'cpio.xz', + 'cramfs', 'elf', 'ext2', 'ext2.bz2', 'ext2.gz', 'ext2.lzma', 'ext4', + 'ext4.gz', 'ext3', 'ext3.gz', 'hddimg', 'iso', 'jffs2', 'jffs2.sum', + 'squashfs', 'squashfs-lzo', 'squashfs-xz', 'tar.bz2', 'tar.lz4', + 'tar.xz', 'tartar.gz', 'ubi', 'ubifs', 'vmdk' + } + target = models.ForeignKey(Target) file_name = models.FilePathField(max_length=254) file_size = models.IntegerField() + @property + def suffix(self): + filename, suffix = os.path.splitext(self.file_name) + suffix = suffix.lstrip('.') + return suffix + class Target_File(models.Model): ITYPE_REGULAR = 1 ITYPE_DIRECTORY = 2 @@ -552,9 +784,23 @@ class Task(models.Model): work_directory = models.FilePathField(max_length=255, blank=True) script_type = models.IntegerField(choices=TASK_CODING, default=CODING_NA) line_number = models.IntegerField(default=0) - disk_io = models.IntegerField(null=True) - cpu_usage = models.DecimalField(max_digits=8, decimal_places=2, null=True) + + # start/end times + started = models.DateTimeField(null=True) + ended = models.DateTimeField(null=True) + + # in seconds; this is stored to enable sorting elapsed_time = models.DecimalField(max_digits=8, decimal_places=2, null=True) + + # in bytes; note that disk_io is stored to enable sorting + disk_io = models.IntegerField(null=True) + disk_io_read = models.IntegerField(null=True) + disk_io_write = models.IntegerField(null=True) + + # in seconds + cpu_time_user = models.DecimalField(max_digits=8, decimal_places=2, null=True) + cpu_time_system = models.DecimalField(max_digits=8, decimal_places=2, null=True) + sstate_result = models.IntegerField(choices=SSTATE_RESULT, default=SSTATE_NA) message = models.CharField(max_length=240) logfile = models.FilePathField(max_length=255, blank=True) @@ -589,11 +835,55 @@ class Package(models.Model): section = models.CharField(max_length=80, blank=True) license = models.CharField(max_length=80, blank=True) + @property + def is_locale_package(self): + """ Returns True if this package is identifiable as a locale package """ + if self.name.find('locale') != -1: + return True + return False + + @property + def is_packagegroup(self): + """ Returns True is this package is identifiable as a packagegroup """ + if self.name.find('packagegroup') != -1: + return True + return False + +class CustomImagePackage(Package): + # CustomImageRecipe fields to track pacakges appended, + # included and excluded from a CustomImageRecipe + recipe_includes = models.ManyToManyField('CustomImageRecipe', + related_name='includes_set') + recipe_excludes = models.ManyToManyField('CustomImageRecipe', + related_name='excludes_set') + recipe_appends = models.ManyToManyField('CustomImageRecipe', + related_name='appends_set') + + + class Package_DependencyManager(models.Manager): use_for_related_fields = True - def get_query_set(self): - return super(Package_DependencyManager, self).get_query_set().exclude(package_id = F('depends_on__id')) + def get_queryset(self): + return super(Package_DependencyManager, self).get_queryset().exclude(package_id = F('depends_on__id')) + + def get_total_source_deps_size(self): + """ Returns the total file size of all the packages that depend on + thispackage. + """ + return self.all().aggregate(Sum('depends_on__size')) + + def get_total_revdeps_size(self): + """ Returns the total file size of all the packages that depend on + this package. + """ + return self.all().aggregate(Sum('package_id__size')) + + + def all_depends(self): + """ Returns just the depends packages and not any other dep_type """ + return self.filter(Q(dep_type=Package_Dependency.TYPE_RDEPENDS) | + Q(dep_type=Package_Dependency.TYPE_TRDEPENDS)) class Package_Dependency(models.Model): TYPE_RDEPENDS = 0 @@ -693,8 +983,12 @@ class Recipe(models.Model): class Recipe_DependencyManager(models.Manager): use_for_related_fields = True - def get_query_set(self): - return super(Recipe_DependencyManager, self).get_query_set().exclude(recipe_id = F('depends_on__id')) + def get_queryset(self): + return super(Recipe_DependencyManager, self).get_queryset().exclude(recipe_id = F('depends_on__id')) + +class Provides(models.Model): + name = models.CharField(max_length=100) + recipe = models.ForeignKey(Recipe) class Recipe_Dependency(models.Model): TYPE_DEPENDS = 0 @@ -706,6 +1000,7 @@ class Recipe_Dependency(models.Model): ) recipe = models.ForeignKey(Recipe, related_name='r_dependencies_recipe') depends_on = models.ForeignKey(Recipe, related_name='r_dependencies_depends') + via = models.ForeignKey(Provides, null=True, default=None) dep_type = models.IntegerField(choices=DEPENDS_TYPE) objects = Recipe_DependencyManager() @@ -895,8 +1190,7 @@ class LayerIndexLayerSource(LayerSource): # update layers layers_info = _get_json_response(apilinks['layerItems']) - if not connection.features.autocommits_when_autocommit_is_off: - transaction.set_autocommit(False) + for li in layers_info: # Special case for the openembedded-core layer if li['name'] == oe_core_layer: @@ -928,17 +1222,12 @@ class LayerIndexLayerSource(LayerSource): l.description = li['description'] l.save() - if not connection.features.autocommits_when_autocommit_is_off: - transaction.set_autocommit(True) - # update layerbranches/layer_versions logger.debug("Fetching layer information") layerbranches_info = _get_json_response(apilinks['layerBranches'] + "?filter=branch:%s" % "OR".join(map(lambda x: str(x.up_id), [i for i in Branch.objects.filter(layer_source = self) if i.up_id is not None] )) ) - if not connection.features.autocommits_when_autocommit_is_off: - transaction.set_autocommit(False) for lbi in layerbranches_info: lv, created = Layer_Version.objects.get_or_create(layer_source = self, up_id = lbi['id'], @@ -951,14 +1240,10 @@ class LayerIndexLayerSource(LayerSource): lv.commit = lbi['actual_branch'] lv.dirpath = lbi['vcs_subdir'] lv.save() - if not connection.features.autocommits_when_autocommit_is_off: - transaction.set_autocommit(True) # update layer dependencies layerdependencies_info = _get_json_response(apilinks['layerDependencies']) dependlist = {} - if not connection.features.autocommits_when_autocommit_is_off: - transaction.set_autocommit(False) for ldi in layerdependencies_info: try: lv = Layer_Version.objects.get(layer_source = self, up_id = ldi['layerbranch']) @@ -976,8 +1261,6 @@ class LayerIndexLayerSource(LayerSource): LayerVersionDependency.objects.filter(layer_version = lv).delete() for lvd in dependlist[lv]: LayerVersionDependency.objects.get_or_create(layer_version = lv, depends_on = lvd) - if not connection.features.autocommits_when_autocommit_is_off: - transaction.set_autocommit(True) # update machines @@ -986,8 +1269,6 @@ class LayerIndexLayerSource(LayerSource): + "?filter=layerbranch:%s" % "OR".join(map(lambda x: str(x.up_id), Layer_Version.objects.filter(layer_source = self))) ) - if not connection.features.autocommits_when_autocommit_is_off: - transaction.set_autocommit(False) for mi in machines_info: mo, created = Machine.objects.get_or_create(layer_source = self, up_id = mi['id'], layer_version = Layer_Version.objects.get(layer_source = self, up_id = mi['layerbranch'])) mo.up_date = mi['updated'] @@ -995,16 +1276,11 @@ class LayerIndexLayerSource(LayerSource): mo.description = mi['description'] mo.save() - if not connection.features.autocommits_when_autocommit_is_off: - transaction.set_autocommit(True) - # update recipes; paginate by layer version / layer branch logger.debug("Fetching target information") recipes_info = _get_json_response(apilinks['recipes'] + "?filter=layerbranch:%s" % "OR".join(map(lambda x: str(x.up_id), Layer_Version.objects.filter(layer_source = self))) ) - if not connection.features.autocommits_when_autocommit_is_off: - transaction.set_autocommit(False) for ri in recipes_info: try: ro, created = Recipe.objects.get_or_create(layer_source = self, up_id = ri['id'], layer_version = Layer_Version.objects.get(layer_source = self, up_id = ri['layerbranch'])) @@ -1026,8 +1302,6 @@ class LayerIndexLayerSource(LayerSource): except IntegrityError as e: logger.debug("Failed saving recipe, ignoring: %s (%s:%s)" % (e, ro.layer_version, ri['filepath']+"/"+ri['filename'])) ro.delete() - if not connection.features.autocommits_when_autocommit_is_off: - transaction.set_autocommit(True) class BitbakeVersion(models.Model): @@ -1110,6 +1384,9 @@ class Layer(models.Model): # LayerCommit class is synced with layerindex.LayerBranch class Layer_Version(models.Model): + """ + A Layer_Version either belongs to a single project or no project + """ search_allowed_fields = ["layer__name", "layer__summary", "layer__description", "layer__vcs_url", "dirpath", "up_branch__name", "commit", "branch"] build = models.ForeignKey(Build, related_name='layer_version_build', default = None, null = True) layer = models.ForeignKey(Layer, related_name='layer_version_layer') @@ -1178,7 +1455,9 @@ class Layer_Version(models.Model): return self._handle_url_path(self.layer.vcs_web_tree_base_url, '') def get_equivalents_wpriority(self, project): - return project.compatible_layerversions(layer_name = self.layer.name) + layer_versions = project.get_all_compatible_layer_versions() + filtered = layer_versions.filter(layer__name = self.layer.name) + return filtered.order_by("-layer_source__releaselayersourcepriority__priority") def get_vcs_reference(self): if self.branch is not None and len(self.branch) > 0: @@ -1187,7 +1466,7 @@ class Layer_Version(models.Model): return self.up_branch.name if self.commit is not None and len(self.commit) > 0: return self.commit - return ("Cannot determine the vcs_reference for layer version %s" % vars(self)) + return 'N/A' def get_detailspage_url(self, project_id): return reverse('layerdetails', args=(project_id, self.pk)) @@ -1238,14 +1517,142 @@ class ProjectLayer(models.Model): class Meta: unique_together = (("project", "layercommit"),) -class CustomImageRecipe(models.Model): - name = models.CharField(max_length=100) - base_recipe = models.ForeignKey(Recipe) - packages = models.ManyToManyField(Package) +class CustomImageRecipe(Recipe): + + # CustomImageRecipe's belong to layers called: + LAYER_NAME = "toaster-custom-images" + + search_allowed_fields = ['name'] + base_recipe = models.ForeignKey(Recipe, related_name='based_on_recipe') project = models.ForeignKey(Project) + last_updated = models.DateTimeField(null=True, default=None) + + def get_last_successful_built_target(self): + """ Return the last successful built target object if one exists + otherwise return None """ + return Target.objects.filter(Q(build__outcome=Build.SUCCEEDED) & + Q(build__project=self.project) & + Q(target=self.name)).last() + + def update_package_list(self): + """ Update the package list from the last good build of this + CustomImageRecipe + """ + # Check if we're aldready up-to-date or not + target = self.get_last_successful_built_target() + if target == None: + # So we've never actually built this Custom recipe but what about + # the recipe it's based on? + target = \ + Target.objects.filter(Q(build__outcome=Build.SUCCEEDED) & + Q(build__project=self.project) & + Q(target=self.base_recipe.name)).last() + if target == None: + return + + if target.build.completed_on == self.last_updated: + return - class Meta: - unique_together = ("name", "project") + self.includes_set.clear() + + excludes_list = self.excludes_set.values_list('name', flat=True) + appends_list = self.appends_set.values_list('name', flat=True) + + built_packages_list = \ + target.target_installed_package_set.values_list('package__name', + flat=True) + for built_package in built_packages_list: + # Is the built package in the custom packages list? + if built_package in excludes_list: + continue + + if built_package in appends_list: + continue + + cust_img_p = \ + CustomImagePackage.objects.get(name=built_package) + self.includes_set.add(cust_img_p) + + + self.last_updated = target.build.completed_on + self.save() + + def get_all_packages(self): + """Get the included packages and any appended packages""" + self.update_package_list() + + return CustomImagePackage.objects.filter((Q(recipe_appends=self) | + Q(recipe_includes=self)) & + ~Q(recipe_excludes=self)) + + + def generate_recipe_file_contents(self): + """Generate the contents for the recipe file.""" + # If we have no excluded packages we only need to _append + if self.excludes_set.count() == 0: + packages_conf = "IMAGE_INSTALL_append = \" " + + for pkg in self.appends_set.all(): + packages_conf += pkg.name+' ' + else: + packages_conf = "IMAGE_FEATURES =\"\"\nIMAGE_INSTALL = \"" + # We add all the known packages to be built by this recipe apart + # from locale packages which are are controlled with IMAGE_LINGUAS. + for pkg in self.get_all_packages().exclude( + name__icontains="locale"): + packages_conf += pkg.name+' ' + + packages_conf += "\"" + try: + base_recipe = open("%s/%s" % + (self.base_recipe.layer_version.dirpath, + self.base_recipe.file_path), 'r').read() + except IOError: + # The path may now be the full path if the recipe has been built + base_recipe = open(self.base_recipe.file_path, 'r').read() + + # Add a special case for when the recipe we have based a custom image + # recipe on requires another recipe. + # For example: + # "require core-image-minimal.bb" is changed to: + # "require recipes-core/images/core-image-minimal.bb" + + req_search = re.search(r'(require\s+)(.+\.bb\s*$)', + base_recipe, + re.MULTILINE) + if req_search: + require_filename = req_search.group(2).strip() + + corrected_location = Recipe.objects.filter( + Q(layer_version=self.base_recipe.layer_version) & + Q(file_path__icontains=require_filename)).last().file_path + + new_require_line = "require %s" % corrected_location + + base_recipe = \ + base_recipe.replace(req_search.group(0), new_require_line) + + + info = {"date" : timezone.now().strftime("%Y-%m-%d %H:%M:%S"), + "base_recipe" : base_recipe, + "recipe_name" : self.name, + "base_recipe_name" : self.base_recipe.name, + "license" : self.license, + "summary" : self.summary, + "description" : self.description, + "packages_conf" : packages_conf.strip(), + } + + recipe_contents = ("# Original recipe %(base_recipe_name)s \n" + "%(base_recipe)s\n\n" + "# Recipe %(recipe_name)s \n" + "# Customisation Generated by Toaster on %(date)s\n" + "SUMMARY = \"%(summary)s\"\n" + "DESCRIPTION = \"%(description)s\"\n" + "LICENSE = \"%(license)s\"\n" + "%(packages_conf)s") % info + + return recipe_contents class ProjectVariable(models.Model): project = models.ForeignKey(Project) @@ -1301,7 +1708,7 @@ class LogMessage(models.Model): lineno = models.IntegerField(null=True) def __str__(self): - return "%s %s %s" % (self.get_level_display(), self.message, self.build) + return force_bytes('%s %s %s' % (self.get_level_display(), self.message, self.build)) def invalidate_cache(**kwargs): from django.core.cache import cache @@ -1312,3 +1719,4 @@ def invalidate_cache(**kwargs): django.db.models.signals.post_save.connect(invalidate_cache) django.db.models.signals.post_delete.connect(invalidate_cache) +django.db.models.signals.m2m_changed.connect(invalidate_cache) |