blob: 7720290214e282b949d5d79738753092fd32d858 [file] [log] [blame]
rjw1f884582022-01-06 17:20:42 +08001#
2# ex:ts=4:sw=4:sts=4:et
3# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
4#
5# BitBake Toaster Implementation
6#
7# Copyright (C) 2013 Intel Corporation
8#
9# This program is free software; you can redistribute it and/or modify
10# it under the terms of the GNU General Public License version 2 as
11# published by the Free Software Foundation.
12#
13# This program is distributed in the hope that it will be useful,
14# but WITHOUT ANY WARRANTY; without even the implied warranty of
15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16# GNU General Public License for more details.
17#
18# You should have received a copy of the GNU General Public License along
19# with this program; if not, write to the Free Software Foundation, Inc.,
20# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
21
22from __future__ import unicode_literals
23
24from django.db import models, IntegrityError, DataError
25from django.db.models import F, Q, Sum, Count
26from django.utils import timezone
27from django.utils.encoding import force_bytes
28
29from django.core.urlresolvers import reverse
30
31from django.core import validators
32from django.conf import settings
33import django.db.models.signals
34
35import sys
36import os
37import re
38import itertools
39from signal import SIGUSR1
40
41
42import logging
43logger = logging.getLogger("toaster")
44
45if 'sqlite' in settings.DATABASES['default']['ENGINE']:
46 from django.db import transaction, OperationalError
47 from time import sleep
48
49 _base_save = models.Model.save
50 def save(self, *args, **kwargs):
51 while True:
52 try:
53 with transaction.atomic():
54 return _base_save(self, *args, **kwargs)
55 except OperationalError as err:
56 if 'database is locked' in str(err):
57 logger.warning("%s, model: %s, args: %s, kwargs: %s",
58 err, self.__class__, args, kwargs)
59 sleep(0.5)
60 continue
61 raise
62
63 models.Model.save = save
64
65 # HACK: Monkey patch Django to fix 'database is locked' issue
66
67 from django.db.models.query import QuerySet
68 _base_insert = QuerySet._insert
69 def _insert(self, *args, **kwargs):
70 with transaction.atomic(using=self.db, savepoint=False):
71 return _base_insert(self, *args, **kwargs)
72 QuerySet._insert = _insert
73
74 from django.utils import six
75 def _create_object_from_params(self, lookup, params):
76 """
77 Tries to create an object using passed params.
78 Used by get_or_create and update_or_create
79 """
80 try:
81 obj = self.create(**params)
82 return obj, True
83 except (IntegrityError, DataError):
84 exc_info = sys.exc_info()
85 try:
86 return self.get(**lookup), False
87 except self.model.DoesNotExist:
88 pass
89 six.reraise(*exc_info)
90
91 QuerySet._create_object_from_params = _create_object_from_params
92
93 # end of HACK
94
95class GitURLValidator(validators.URLValidator):
96 import re
97 regex = re.compile(
98 r'^(?:ssh|git|http|ftp)s?://' # http:// or https://
99 r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|' # domain...
100 r'localhost|' # localhost...
101 r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|' # ...or ipv4
102 r'\[?[A-F0-9]*:[A-F0-9:]+\]?)' # ...or ipv6
103 r'(?::\d+)?' # optional port
104 r'(?:/?|[/?]\S+)$', re.IGNORECASE)
105
106def GitURLField(**kwargs):
107 r = models.URLField(**kwargs)
108 for i in range(len(r.validators)):
109 if isinstance(r.validators[i], validators.URLValidator):
110 r.validators[i] = GitURLValidator()
111 return r
112
113
114class ToasterSetting(models.Model):
115 name = models.CharField(max_length=63)
116 helptext = models.TextField()
117 value = models.CharField(max_length=255)
118
119 def __unicode__(self):
120 return "Setting %s = %s" % (self.name, self.value)
121
122
123class ProjectManager(models.Manager):
124 def create_project(self, name, release, existing_project=None):
125 if existing_project and (release is not None):
126 prj = existing_project
127 prj.bitbake_version = release.bitbake_version
128 prj.release = release
129 # Delete the previous ProjectLayer mappings
130 for pl in ProjectLayer.objects.filter(project=prj):
131 pl.delete()
132 elif release is not None:
133 prj = self.model(name=name,
134 bitbake_version=release.bitbake_version,
135 release=release)
136 else:
137 prj = self.model(name=name,
138 bitbake_version=None,
139 release=None)
140 prj.save()
141
142 for defaultconf in ToasterSetting.objects.filter(
143 name__startswith="DEFCONF_"):
144 name = defaultconf.name[8:]
145 pv,create = ProjectVariable.objects.get_or_create(project=prj,name=name)
146 pv.value = defaultconf.value
147 pv.save()
148
149 if release is None:
150 return prj
151
152 for rdl in release.releasedefaultlayer_set.all():
153 lv = Layer_Version.objects.filter(
154 layer__name=rdl.layer_name,
155 release=release).first()
156
157 if lv:
158 ProjectLayer.objects.create(project=prj,
159 layercommit=lv,
160 optional=False)
161 else:
162 logger.warning("Default project layer %s not found" %
163 rdl.layer_name)
164
165 return prj
166
167 # return single object with is_default = True
168 def get_or_create_default_project(self):
169 projects = super(ProjectManager, self).filter(is_default=True)
170
171 if len(projects) > 1:
172 raise Exception('Inconsistent project data: multiple ' +
173 'default projects (i.e. with is_default=True)')
174 elif len(projects) < 1:
175 options = {
176 'name': 'Command line builds',
177 'short_description':
178 'Project for builds started outside Toaster',
179 'is_default': True
180 }
181 project = Project.objects.create(**options)
182 project.save()
183
184 return project
185 else:
186 return projects[0]
187
188
189class Project(models.Model):
190 search_allowed_fields = ['name', 'short_description', 'release__name',
191 'release__branch_name']
192 name = models.CharField(max_length=100)
193 short_description = models.CharField(max_length=50, blank=True)
194 bitbake_version = models.ForeignKey('BitbakeVersion', null=True)
195 release = models.ForeignKey("Release", null=True)
196 created = models.DateTimeField(auto_now_add=True)
197 updated = models.DateTimeField(auto_now=True)
198 # This is a horrible hack; since Toaster has no "User" model available when
199 # running in interactive mode, we can't reference the field here directly
200 # Instead, we keep a possible null reference to the User id,
201 # as not to force
202 # hard links to possibly missing models
203 user_id = models.IntegerField(null=True)
204 objects = ProjectManager()
205
206 # build directory override (e.g. imported)
207 builddir = models.TextField()
208 # merge the Toaster configure attributes directly into the standard conf files
209 merged_attr = models.BooleanField(default=False)
210
211 # set to True for the project which is the default container
212 # for builds initiated by the command line etc.
213 is_default= models.BooleanField(default=False)
214
215 def __unicode__(self):
216 return "%s (Release %s, BBV %s)" % (self.name, self.release, self.bitbake_version)
217
218 def get_current_machine_name(self):
219 try:
220 return self.projectvariable_set.get(name="MACHINE").value
221 except (ProjectVariable.DoesNotExist,IndexError):
222 return None;
223
224 def get_number_of_builds(self):
225 """Return the number of builds which have ended"""
226
227 return self.build_set.exclude(
228 Q(outcome=Build.IN_PROGRESS) |
229 Q(outcome=Build.CANCELLED)
230 ).count()
231
232 def get_last_build_id(self):
233 try:
234 return Build.objects.filter( project = self.id ).order_by('-completed_on')[0].id
235 except (Build.DoesNotExist,IndexError):
236 return( -1 )
237
238 def get_last_outcome(self):
239 build_id = self.get_last_build_id()
240 if (-1 == build_id):
241 return( "" )
242 try:
243 return Build.objects.filter( id = build_id )[ 0 ].outcome
244 except (Build.DoesNotExist,IndexError):
245 return( "not_found" )
246
247 def get_last_target(self):
248 build_id = self.get_last_build_id()
249 if (-1 == build_id):
250 return( "" )
251 try:
252 return Target.objects.filter(build = build_id)[0].target
253 except (Target.DoesNotExist,IndexError):
254 return( "not_found" )
255
256 def get_last_errors(self):
257 build_id = self.get_last_build_id()
258 if (-1 == build_id):
259 return( 0 )
260 try:
261 return Build.objects.filter(id = build_id)[ 0 ].errors.count()
262 except (Build.DoesNotExist,IndexError):
263 return( "not_found" )
264
265 def get_last_warnings(self):
266 build_id = self.get_last_build_id()
267 if (-1 == build_id):
268 return( 0 )
269 try:
270 return Build.objects.filter(id = build_id)[ 0 ].warnings.count()
271 except (Build.DoesNotExist,IndexError):
272 return( "not_found" )
273
274 def get_last_build_extensions(self):
275 """
276 Get list of file name extensions for images produced by the most
277 recent build
278 """
279 last_build = Build.objects.get(pk = self.get_last_build_id())
280 return last_build.get_image_file_extensions()
281
282 def get_last_imgfiles(self):
283 build_id = self.get_last_build_id()
284 if (-1 == build_id):
285 return( "" )
286 try:
287 return Variable.objects.filter(build = build_id, variable_name = "IMAGE_FSTYPES")[ 0 ].variable_value
288 except (Variable.DoesNotExist,IndexError):
289 return( "not_found" )
290
291 def get_all_compatible_layer_versions(self):
292 """ Returns Queryset of all Layer_Versions which are compatible with
293 this project"""
294 queryset = None
295
296 # guard on release, as it can be null
297 if self.release:
298 queryset = Layer_Version.objects.filter(
299 (Q(release=self.release) &
300 Q(build=None) &
301 Q(project=None)) |
302 Q(project=self))
303 else:
304 queryset = Layer_Version.objects.none()
305
306 return queryset
307
308 def get_project_layer_versions(self, pk=False):
309 """ Returns the Layer_Versions currently added to this project """
310 layer_versions = self.projectlayer_set.all().values_list('layercommit',
311 flat=True)
312
313 if pk is False:
314 return Layer_Version.objects.filter(pk__in=layer_versions)
315 else:
316 return layer_versions
317
318
319 def get_default_image_recipe(self):
320 try:
321 return self.projectvariable_set.get(name="DEFAULT_IMAGE").value
322 except (ProjectVariable.DoesNotExist,IndexError):
323 return None;
324
325 def get_is_new(self):
326 return self.get_variable(Project.PROJECT_SPECIFIC_ISNEW)
327
328 def get_available_machines(self):
329 """ Returns QuerySet of all Machines which are provided by the
330 Layers currently added to the Project """
331 queryset = Machine.objects.filter(
332 layer_version__in=self.get_project_layer_versions())
333
334 return queryset
335
336 def get_all_compatible_machines(self):
337 """ Returns QuerySet of all the compatible machines available to the
338 project including ones from Layers not currently added """
339 queryset = Machine.objects.filter(
340 layer_version__in=self.get_all_compatible_layer_versions())
341
342 return queryset
343
344 def get_available_distros(self):
345 """ Returns QuerySet of all Distros which are provided by the
346 Layers currently added to the Project """
347 queryset = Distro.objects.filter(
348 layer_version__in=self.get_project_layer_versions())
349
350 return queryset
351
352 def get_all_compatible_distros(self):
353 """ Returns QuerySet of all the compatible Wind River distros available to the
354 project including ones from Layers not currently added """
355 queryset = Distro.objects.filter(
356 layer_version__in=self.get_all_compatible_layer_versions())
357
358 return queryset
359
360 def get_available_recipes(self):
361 """ Returns QuerySet of all the recipes that are provided by layers
362 added to this project """
363 queryset = Recipe.objects.filter(
364 layer_version__in=self.get_project_layer_versions())
365
366 return queryset
367
368 def get_all_compatible_recipes(self):
369 """ Returns QuerySet of all the compatible Recipes available to the
370 project including ones from Layers not currently added """
371 queryset = Recipe.objects.filter(
372 layer_version__in=self.get_all_compatible_layer_versions()).exclude(name__exact='')
373
374 return queryset
375
376 # Project Specific status management
377 PROJECT_SPECIFIC_STATUS = 'INTERNAL_PROJECT_SPECIFIC_STATUS'
378 PROJECT_SPECIFIC_CALLBACK = 'INTERNAL_PROJECT_SPECIFIC_CALLBACK'
379 PROJECT_SPECIFIC_ISNEW = 'INTERNAL_PROJECT_SPECIFIC_ISNEW'
380 PROJECT_SPECIFIC_DEFAULTIMAGE = 'PROJECT_SPECIFIC_DEFAULTIMAGE'
381 PROJECT_SPECIFIC_NONE = ''
382 PROJECT_SPECIFIC_NEW = '1'
383 PROJECT_SPECIFIC_EDIT = '2'
384 PROJECT_SPECIFIC_CLONING = '3'
385 PROJECT_SPECIFIC_CLONING_SUCCESS = '4'
386 PROJECT_SPECIFIC_CLONING_FAIL = '5'
387
388 def get_variable(self,variable,default_value = ''):
389 try:
390 return self.projectvariable_set.get(name=variable).value
391 except (ProjectVariable.DoesNotExist,IndexError):
392 return default_value
393
394 def set_variable(self,variable,value):
395 pv,create = ProjectVariable.objects.get_or_create(project = self, name = variable)
396 pv.value = value
397 pv.save()
398
399 def get_default_image(self):
400 return self.get_variable(Project.PROJECT_SPECIFIC_DEFAULTIMAGE)
401
402 def schedule_build(self):
403
404 from bldcontrol.models import BuildRequest, BRTarget, BRLayer
405 from bldcontrol.models import BRBitbake, BRVariable
406
407 try:
408 now = timezone.now()
409 build = Build.objects.create(project=self,
410 completed_on=now,
411 started_on=now)
412
413 br = BuildRequest.objects.create(project=self,
414 state=BuildRequest.REQ_QUEUED,
415 build=build)
416 BRBitbake.objects.create(req=br,
417 giturl=self.bitbake_version.giturl,
418 commit=self.bitbake_version.branch,
419 dirpath=self.bitbake_version.dirpath)
420
421 for t in self.projecttarget_set.all():
422 BRTarget.objects.create(req=br, target=t.target, task=t.task)
423 Target.objects.create(build=br.build, target=t.target,
424 task=t.task)
425 # If we're about to build a custom image recipe make sure
426 # that layer is currently in the project before we create the
427 # BRLayer objects
428 customrecipe = CustomImageRecipe.objects.filter(
429 name=t.target,
430 project=self).first()
431 if customrecipe:
432 ProjectLayer.objects.get_or_create(
433 project=self,
434 layercommit=customrecipe.layer_version,
435 optional=False)
436
437 for l in self.projectlayer_set.all().order_by("pk"):
438 commit = l.layercommit.get_vcs_reference()
439 logger.debug("Adding layer to build %s" %
440 l.layercommit.layer.name)
441 BRLayer.objects.create(
442 req=br,
443 name=l.layercommit.layer.name,
444 giturl=l.layercommit.layer.vcs_url,
445 commit=commit,
446 dirpath=l.layercommit.dirpath,
447 layer_version=l.layercommit,
448 local_source_dir=l.layercommit.layer.local_source_dir
449 )
450
451 for v in self.projectvariable_set.all():
452 BRVariable.objects.create(req=br, name=v.name, value=v.value)
453
454 try:
455 br.build.machine = self.projectvariable_set.get(
456 name='MACHINE').value
457 br.build.save()
458 except ProjectVariable.DoesNotExist:
459 pass
460
461 br.save()
462 signal_runbuilds()
463
464 except Exception:
465 # revert the build request creation since we're not done cleanly
466 br.delete()
467 raise
468 return br
469
470class Build(models.Model):
471 SUCCEEDED = 0
472 FAILED = 1
473 IN_PROGRESS = 2
474 CANCELLED = 3
475
476 BUILD_OUTCOME = (
477 (SUCCEEDED, 'Succeeded'),
478 (FAILED, 'Failed'),
479 (IN_PROGRESS, 'In Progress'),
480 (CANCELLED, 'Cancelled'),
481 )
482
483 search_allowed_fields = ['machine', 'cooker_log_path', "target__target", "target__target_image_file__file_name"]
484
485 project = models.ForeignKey(Project) # must have a project
486 machine = models.CharField(max_length=100)
487 distro = models.CharField(max_length=100)
488 distro_version = models.CharField(max_length=100)
489 started_on = models.DateTimeField()
490 completed_on = models.DateTimeField()
491 outcome = models.IntegerField(choices=BUILD_OUTCOME, default=IN_PROGRESS)
492 cooker_log_path = models.CharField(max_length=500)
493 build_name = models.CharField(max_length=100, default='')
494 bitbake_version = models.CharField(max_length=50)
495
496 # number of recipes to parse for this build
497 recipes_to_parse = models.IntegerField(default=1)
498
499 # number of recipes parsed so far for this build
500 recipes_parsed = models.IntegerField(default=1)
501
502 # number of repos to clone for this build
503 repos_to_clone = models.IntegerField(default=1)
504
505 # number of repos cloned so far for this build (default off)
506 repos_cloned = models.IntegerField(default=1)
507
508 # Hint on current progress item
509 progress_item = models.CharField(max_length=40)
510
511 @staticmethod
512 def get_recent(project=None):
513 """
514 Return recent builds as a list; if project is set, only return
515 builds for that project
516 """
517
518 builds = Build.objects.all()
519
520 if project:
521 builds = builds.filter(project=project)
522
523 finished_criteria = \
524 Q(outcome=Build.SUCCEEDED) | \
525 Q(outcome=Build.FAILED) | \
526 Q(outcome=Build.CANCELLED)
527
528 recent_builds = list(itertools.chain(
529 builds.filter(outcome=Build.IN_PROGRESS).order_by("-started_on"),
530 builds.filter(finished_criteria).order_by("-completed_on")[:3]
531 ))
532
533 # add percentage done property to each build; this is used
534 # to show build progress in mrb_section.html
535 for build in recent_builds:
536 build.percentDone = build.completeper()
537 build.outcomeText = build.get_outcome_text()
538
539 return recent_builds
540
541 def started(self):
542 """
543 As build variables are only added for a build when its BuildStarted event
544 is received, a build with no build variables is counted as
545 "in preparation" and not properly started yet. This method
546 will return False if a build has no build variables (it never properly
547 started), or True otherwise.
548
549 Note that this is a temporary workaround for the fact that we don't
550 have a fine-grained state variable on a build which would allow us
551 to record "in progress" (BuildStarted received) vs. "in preparation".
552 """
553 variables = Variable.objects.filter(build=self)
554 return len(variables) > 0
555
556 def completeper(self):
557 tf = Task.objects.filter(build = self)
558 tfc = tf.count()
559 if tfc > 0:
560 completeper = tf.exclude(outcome=Task.OUTCOME_NA).count()*100 // tfc
561 else:
562 completeper = 0
563 return completeper
564
565 def eta(self):
566 eta = timezone.now()
567 completeper = self.completeper()
568 if self.completeper() > 0:
569 eta += ((eta - self.started_on)*(100-completeper))/completeper
570 return eta
571
572 def has_images(self):
573 """
574 Returns True if at least one of the targets for this build has an
575 image file associated with it, False otherwise
576 """
577 targets = Target.objects.filter(build_id=self.id)
578 has_images = False
579 for target in targets:
580 if target.has_images():
581 has_images = True
582 break
583 return has_images
584
585 def has_image_recipes(self):
586 """
587 Returns True if a build has any targets which were built from
588 image recipes.
589 """
590 image_recipes = self.get_image_recipes()
591 return len(image_recipes) > 0
592
593 def get_image_file_extensions(self):
594 """
595 Get string of file name extensions for images produced by this build;
596 note that this is the actual list of extensions stored on Target objects
597 for this build, and not the value of IMAGE_FSTYPES.
598
599 Returns comma-separated string, e.g. "vmdk, ext4"
600 """
601 extensions = []
602
603 targets = Target.objects.filter(build_id = self.id)
604 for target in targets:
605 if not target.is_image:
606 continue
607
608 target_image_files = Target_Image_File.objects.filter(
609 target_id=target.id)
610
611 for target_image_file in target_image_files:
612 extensions.append(target_image_file.suffix)
613
614 extensions = list(set(extensions))
615 extensions.sort()
616
617 return ', '.join(extensions)
618
619 def get_image_fstypes(self):
620 """
621 Get the IMAGE_FSTYPES variable value for this build as a de-duplicated
622 list of image file suffixes.
623 """
624 image_fstypes = Variable.objects.get(
625 build=self, variable_name='IMAGE_FSTYPES').variable_value
626 return list(set(re.split(r' {1,}', image_fstypes)))
627
628 def get_sorted_target_list(self):
629 tgts = Target.objects.filter(build_id = self.id).order_by( 'target' );
630 return( tgts );
631
632 def get_recipes(self):
633 """
634 Get the recipes related to this build;
635 note that the related layer versions and layers are also prefetched
636 by this query, as this queryset can be sorted by these objects in the
637 build recipes view; prefetching them here removes the need
638 for another query in that view
639 """
640 layer_versions = Layer_Version.objects.filter(build=self)
641 criteria = Q(layer_version__id__in=layer_versions)
642 return Recipe.objects.filter(criteria) \
643 .select_related('layer_version', 'layer_version__layer')
644
645 def get_image_recipes(self):
646 """
647 Returns a list of image Recipes (custom and built-in) related to this
648 build, sorted by name; note that this has to be done in two steps, as
649 there's no way to get all the custom image recipes and image recipes
650 in one query
651 """
652 custom_image_recipes = self.get_custom_image_recipes()
653 custom_image_recipe_names = custom_image_recipes.values_list('name', flat=True)
654
655 not_custom_image_recipes = ~Q(name__in=custom_image_recipe_names) & \
656 Q(is_image=True)
657
658 built_image_recipes = self.get_recipes().filter(not_custom_image_recipes)
659
660 # append to the custom image recipes and sort
661 customisable_image_recipes = list(
662 itertools.chain(custom_image_recipes, built_image_recipes)
663 )
664
665 return sorted(customisable_image_recipes, key=lambda recipe: recipe.name)
666
667 def get_custom_image_recipes(self):
668 """
669 Returns a queryset of CustomImageRecipes related to this build,
670 sorted by name
671 """
672 built_recipe_names = self.get_recipes().values_list('name', flat=True)
673 criteria = Q(name__in=built_recipe_names) & Q(project=self.project)
674 queryset = CustomImageRecipe.objects.filter(criteria).order_by('name')
675 return queryset
676
677 def get_outcome_text(self):
678 return Build.BUILD_OUTCOME[int(self.outcome)][1]
679
680 @property
681 def failed_tasks(self):
682 """ Get failed tasks for the build """
683 tasks = self.task_build.all()
684 return tasks.filter(order__gt=0, outcome=Task.OUTCOME_FAILED)
685
686 @property
687 def errors(self):
688 return (self.logmessage_set.filter(level=LogMessage.ERROR) |
689 self.logmessage_set.filter(level=LogMessage.EXCEPTION) |
690 self.logmessage_set.filter(level=LogMessage.CRITICAL))
691
692 @property
693 def warnings(self):
694 return self.logmessage_set.filter(level=LogMessage.WARNING)
695
696 @property
697 def timespent(self):
698 return self.completed_on - self.started_on
699
700 @property
701 def timespent_seconds(self):
702 return self.timespent.total_seconds()
703
704 @property
705 def target_labels(self):
706 """
707 Sorted (a-z) "target1:task, target2, target3" etc. string for all
708 targets in this build
709 """
710 targets = self.target_set.all()
711 target_labels = [target.target +
712 (':' + target.task if target.task else '')
713 for target in targets]
714 target_labels.sort()
715
716 return target_labels
717
718 def get_buildrequest(self):
719 buildrequest = None
720 if hasattr(self, 'buildrequest'):
721 buildrequest = self.buildrequest
722 return buildrequest
723
724 def is_queued(self):
725 from bldcontrol.models import BuildRequest
726 buildrequest = self.get_buildrequest()
727 if buildrequest:
728 return buildrequest.state == BuildRequest.REQ_QUEUED
729 else:
730 return False
731
732 def is_cancelling(self):
733 from bldcontrol.models import BuildRequest
734 buildrequest = self.get_buildrequest()
735 if buildrequest:
736 return self.outcome == Build.IN_PROGRESS and \
737 buildrequest.state == BuildRequest.REQ_CANCELLING
738 else:
739 return False
740
741 def is_cloning(self):
742 """
743 True if the build is still cloning repos
744 """
745 return self.outcome == Build.IN_PROGRESS and \
746 self.repos_cloned < self.repos_to_clone
747
748 def is_parsing(self):
749 """
750 True if the build is still parsing recipes
751 """
752 return self.outcome == Build.IN_PROGRESS and \
753 self.recipes_parsed < self.recipes_to_parse
754
755 def is_starting(self):
756 """
757 True if the build has no completed tasks yet and is still just starting
758 tasks.
759
760 Note that the mechanism for testing whether a Task is "done" is whether
761 its outcome field is set, as per the completeper() method.
762 """
763 return self.outcome == Build.IN_PROGRESS and \
764 self.task_build.exclude(outcome=Task.OUTCOME_NA).count() == 0
765
766
767 def get_state(self):
768 """
769 Get the state of the build; one of 'Succeeded', 'Failed', 'In Progress',
770 'Cancelled' (Build outcomes); or 'Queued', 'Cancelling' (states
771 dependent on the BuildRequest state).
772
773 This works around the fact that we have BuildRequest states as well
774 as Build states, but really we just want to know the state of the build.
775 """
776 if self.is_cancelling():
777 return 'Cancelling';
778 elif self.is_queued():
779 return 'Queued'
780 elif self.is_cloning():
781 return 'Cloning'
782 elif self.is_parsing():
783 return 'Parsing'
784 elif self.is_starting():
785 return 'Starting'
786 else:
787 return self.get_outcome_text()
788
789 def __str__(self):
790 return "%d %s %s" % (self.id, self.project, ",".join([t.target for t in self.target_set.all()]))
791
792class ProjectTarget(models.Model):
793 project = models.ForeignKey(Project)
794 target = models.CharField(max_length=100)
795 task = models.CharField(max_length=100, null=True)
796
797class Target(models.Model):
798 search_allowed_fields = ['target', 'file_name']
799 build = models.ForeignKey(Build)
800 target = models.CharField(max_length=100)
801 task = models.CharField(max_length=100, null=True)
802 is_image = models.BooleanField(default = False)
803 image_size = models.IntegerField(default=0)
804 license_manifest_path = models.CharField(max_length=500, null=True)
805 package_manifest_path = models.CharField(max_length=500, null=True)
806
807 def package_count(self):
808 return Target_Installed_Package.objects.filter(target_id__exact=self.id).count()
809
810 def __unicode__(self):
811 return self.target
812
813 def get_similar_targets(self):
814 """
815 Get target sfor the same machine, task and target name
816 (e.g. 'core-image-minimal') from a successful build for this project
817 (but excluding this target).
818
819 Note that we only look for targets built by this project because
820 projects can have different configurations from each other, and put
821 their artifacts in different directories.
822
823 The possibility of error when retrieving candidate targets
824 is minimised by the fact that bitbake will rebuild artifacts if MACHINE
825 (or various other variables) change. In this case, there is no need to
826 clone artifacts from another target, as those artifacts will have
827 been re-generated for this target anyway.
828 """
829 query = ~Q(pk=self.pk) & \
830 Q(target=self.target) & \
831 Q(build__machine=self.build.machine) & \
832 Q(build__outcome=Build.SUCCEEDED) & \
833 Q(build__project=self.build.project)
834
835 return Target.objects.filter(query)
836
837 def get_similar_target_with_image_files(self):
838 """
839 Get the most recent similar target with Target_Image_Files associated
840 with it, for the purpose of cloning those files onto this target.
841 """
842 similar_target = None
843
844 candidates = self.get_similar_targets()
845 if candidates.count() == 0:
846 return similar_target
847
848 task_subquery = Q(task=self.task)
849
850 # we can look for a 'build' task if this task is a 'populate_sdk_ext'
851 # task, as the latter also creates images; and vice versa; note that
852 # 'build' targets can have their task set to '';
853 # also note that 'populate_sdk' does not produce image files
854 image_tasks = [
855 '', # aka 'build'
856 'build',
857 'image',
858 'populate_sdk_ext'
859 ]
860 if self.task in image_tasks:
861 task_subquery = Q(task__in=image_tasks)
862
863 # annotate with the count of files, to exclude any targets which
864 # don't have associated files
865 candidates = candidates.annotate(num_files=Count('target_image_file'))
866
867 query = task_subquery & Q(num_files__gt=0)
868
869 candidates = candidates.filter(query)
870
871 if candidates.count() > 0:
872 candidates.order_by('build__completed_on')
873 similar_target = candidates.last()
874
875 return similar_target
876
877 def get_similar_target_with_sdk_files(self):
878 """
879 Get the most recent similar target with TargetSDKFiles associated
880 with it, for the purpose of cloning those files onto this target.
881 """
882 similar_target = None
883
884 candidates = self.get_similar_targets()
885 if candidates.count() == 0:
886 return similar_target
887
888 # annotate with the count of files, to exclude any targets which
889 # don't have associated files
890 candidates = candidates.annotate(num_files=Count('targetsdkfile'))
891
892 query = Q(task=self.task) & Q(num_files__gt=0)
893
894 candidates = candidates.filter(query)
895
896 if candidates.count() > 0:
897 candidates.order_by('build__completed_on')
898 similar_target = candidates.last()
899
900 return similar_target
901
902 def clone_image_artifacts_from(self, target):
903 """
904 Make clones of the Target_Image_Files and TargetKernelFile objects
905 associated with Target target, then associate them with this target.
906
907 Note that for Target_Image_Files, we only want files from the previous
908 build whose suffix matches one of the suffixes defined in this
909 target's build's IMAGE_FSTYPES configuration variable. This prevents the
910 Target_Image_File object for an ext4 image being associated with a
911 target for a project which didn't produce an ext4 image (for example).
912
913 Also sets the license_manifest_path and package_manifest_path
914 of this target to the same path as that of target being cloned from, as
915 the manifests are also build artifacts but are treated differently.
916 """
917
918 image_fstypes = self.build.get_image_fstypes()
919
920 # filter out any image files whose suffixes aren't in the
921 # IMAGE_FSTYPES suffixes variable for this target's build
922 image_files = [target_image_file \
923 for target_image_file in target.target_image_file_set.all() \
924 if target_image_file.suffix in image_fstypes]
925
926 for image_file in image_files:
927 image_file.pk = None
928 image_file.target = self
929 image_file.save()
930
931 kernel_files = target.targetkernelfile_set.all()
932 for kernel_file in kernel_files:
933 kernel_file.pk = None
934 kernel_file.target = self
935 kernel_file.save()
936
937 self.license_manifest_path = target.license_manifest_path
938 self.package_manifest_path = target.package_manifest_path
939 self.save()
940
941 def clone_sdk_artifacts_from(self, target):
942 """
943 Clone TargetSDKFile objects from target and associate them with this
944 target.
945 """
946 sdk_files = target.targetsdkfile_set.all()
947 for sdk_file in sdk_files:
948 sdk_file.pk = None
949 sdk_file.target = self
950 sdk_file.save()
951
952 def has_images(self):
953 """
954 Returns True if this target has one or more image files attached to it.
955 """
956 return self.target_image_file_set.all().count() > 0
957
958# kernel artifacts for a target: bzImage and modules*
959class TargetKernelFile(models.Model):
960 target = models.ForeignKey(Target)
961 file_name = models.FilePathField()
962 file_size = models.IntegerField()
963
964 @property
965 def basename(self):
966 return os.path.basename(self.file_name)
967
968# SDK artifacts for a target: sh and manifest files
969class TargetSDKFile(models.Model):
970 target = models.ForeignKey(Target)
971 file_name = models.FilePathField()
972 file_size = models.IntegerField()
973
974 @property
975 def basename(self):
976 return os.path.basename(self.file_name)
977
978class Target_Image_File(models.Model):
979 # valid suffixes for image files produced by a build
980 SUFFIXES = {
981 'btrfs', 'cpio', 'cpio.gz', 'cpio.lz4', 'cpio.lzma', 'cpio.xz',
982 'cramfs', 'elf', 'ext2', 'ext2.bz2', 'ext2.gz', 'ext2.lzma', 'ext4',
983 'ext4.gz', 'ext3', 'ext3.gz', 'hdddirect', 'hddimg', 'iso', 'jffs2',
984 'jffs2.sum', 'multiubi', 'qcow2', 'squashfs', 'squashfs-lzo',
985 'squashfs-xz', 'tar', 'tar.bz2', 'tar.gz', 'tar.lz4', 'tar.xz', 'ubi',
986 'ubifs', 'vdi', 'vmdk', 'wic', 'wic.bmap', 'wic.bz2', 'wic.gz', 'wic.lzma'
987 }
988
989 target = models.ForeignKey(Target)
990 file_name = models.FilePathField(max_length=254)
991 file_size = models.IntegerField()
992
993 @property
994 def suffix(self):
995 """
996 Suffix for image file, minus leading "."
997 """
998 for suffix in Target_Image_File.SUFFIXES:
999 if self.file_name.endswith(suffix):
1000 return suffix
1001
1002 filename, suffix = os.path.splitext(self.file_name)
1003 suffix = suffix.lstrip('.')
1004 return suffix
1005
1006class Target_File(models.Model):
1007 ITYPE_REGULAR = 1
1008 ITYPE_DIRECTORY = 2
1009 ITYPE_SYMLINK = 3
1010 ITYPE_SOCKET = 4
1011 ITYPE_FIFO = 5
1012 ITYPE_CHARACTER = 6
1013 ITYPE_BLOCK = 7
1014 ITYPES = ( (ITYPE_REGULAR ,'regular'),
1015 ( ITYPE_DIRECTORY ,'directory'),
1016 ( ITYPE_SYMLINK ,'symlink'),
1017 ( ITYPE_SOCKET ,'socket'),
1018 ( ITYPE_FIFO ,'fifo'),
1019 ( ITYPE_CHARACTER ,'character'),
1020 ( ITYPE_BLOCK ,'block'),
1021 )
1022
1023 target = models.ForeignKey(Target)
1024 path = models.FilePathField()
1025 size = models.IntegerField()
1026 inodetype = models.IntegerField(choices = ITYPES)
1027 permission = models.CharField(max_length=16)
1028 owner = models.CharField(max_length=128)
1029 group = models.CharField(max_length=128)
1030 directory = models.ForeignKey('Target_File', related_name="directory_set", null=True)
1031 sym_target = models.ForeignKey('Target_File', related_name="symlink_set", null=True)
1032
1033
1034class Task(models.Model):
1035
1036 SSTATE_NA = 0
1037 SSTATE_MISS = 1
1038 SSTATE_FAILED = 2
1039 SSTATE_RESTORED = 3
1040
1041 SSTATE_RESULT = (
1042 (SSTATE_NA, 'Not Applicable'), # For rest of tasks, but they still need checking.
1043 (SSTATE_MISS, 'File not in cache'), # the sstate object was not found
1044 (SSTATE_FAILED, 'Failed'), # there was a pkg, but the script failed
1045 (SSTATE_RESTORED, 'Succeeded'), # successfully restored
1046 )
1047
1048 CODING_NA = 0
1049 CODING_PYTHON = 2
1050 CODING_SHELL = 3
1051
1052 TASK_CODING = (
1053 (CODING_NA, 'N/A'),
1054 (CODING_PYTHON, 'Python'),
1055 (CODING_SHELL, 'Shell'),
1056 )
1057
1058 OUTCOME_NA = -1
1059 OUTCOME_SUCCESS = 0
1060 OUTCOME_COVERED = 1
1061 OUTCOME_CACHED = 2
1062 OUTCOME_PREBUILT = 3
1063 OUTCOME_FAILED = 4
1064 OUTCOME_EMPTY = 5
1065
1066 TASK_OUTCOME = (
1067 (OUTCOME_NA, 'Not Available'),
1068 (OUTCOME_SUCCESS, 'Succeeded'),
1069 (OUTCOME_COVERED, 'Covered'),
1070 (OUTCOME_CACHED, 'Cached'),
1071 (OUTCOME_PREBUILT, 'Prebuilt'),
1072 (OUTCOME_FAILED, 'Failed'),
1073 (OUTCOME_EMPTY, 'Empty'),
1074 )
1075
1076 TASK_OUTCOME_HELP = (
1077 (OUTCOME_SUCCESS, 'This task successfully completed'),
1078 (OUTCOME_COVERED, 'This task did not run because its output is provided by another task'),
1079 (OUTCOME_CACHED, 'This task restored output from the sstate-cache directory or mirrors'),
1080 (OUTCOME_PREBUILT, 'This task did not run because its outcome was reused from a previous build'),
1081 (OUTCOME_FAILED, 'This task did not complete'),
1082 (OUTCOME_EMPTY, 'This task has no executable content'),
1083 (OUTCOME_NA, ''),
1084 )
1085
1086 search_allowed_fields = [ "recipe__name", "recipe__version", "task_name", "logfile" ]
1087
1088 def __init__(self, *args, **kwargs):
1089 super(Task, self).__init__(*args, **kwargs)
1090 try:
1091 self._helptext = HelpText.objects.get(key=self.task_name, area=HelpText.VARIABLE, build=self.build).text
1092 except HelpText.DoesNotExist:
1093 self._helptext = None
1094
1095 def get_related_setscene(self):
1096 return Task.objects.filter(task_executed=True, build = self.build, recipe = self.recipe, task_name=self.task_name+"_setscene")
1097
1098 def get_outcome_text(self):
1099 return Task.TASK_OUTCOME[int(self.outcome) + 1][1]
1100
1101 def get_outcome_help(self):
1102 return Task.TASK_OUTCOME_HELP[int(self.outcome)][1]
1103
1104 def get_sstate_text(self):
1105 if self.sstate_result==Task.SSTATE_NA:
1106 return ''
1107 else:
1108 return Task.SSTATE_RESULT[int(self.sstate_result)][1]
1109
1110 def get_executed_display(self):
1111 if self.task_executed:
1112 return "Executed"
1113 return "Not Executed"
1114
1115 def get_description(self):
1116 return self._helptext
1117
1118 build = models.ForeignKey(Build, related_name='task_build')
1119 order = models.IntegerField(null=True)
1120 task_executed = models.BooleanField(default=False) # True means Executed, False means Not/Executed
1121 outcome = models.IntegerField(choices=TASK_OUTCOME, default=OUTCOME_NA)
1122 sstate_checksum = models.CharField(max_length=100, blank=True)
1123 path_to_sstate_obj = models.FilePathField(max_length=500, blank=True)
1124 recipe = models.ForeignKey('Recipe', related_name='tasks')
1125 task_name = models.CharField(max_length=100)
1126 source_url = models.FilePathField(max_length=255, blank=True)
1127 work_directory = models.FilePathField(max_length=255, blank=True)
1128 script_type = models.IntegerField(choices=TASK_CODING, default=CODING_NA)
1129 line_number = models.IntegerField(default=0)
1130
1131 # start/end times
1132 started = models.DateTimeField(null=True)
1133 ended = models.DateTimeField(null=True)
1134
1135 # in seconds; this is stored to enable sorting
1136 elapsed_time = models.DecimalField(max_digits=8, decimal_places=2, null=True)
1137
1138 # in bytes; note that disk_io is stored to enable sorting
1139 disk_io = models.IntegerField(null=True)
1140 disk_io_read = models.IntegerField(null=True)
1141 disk_io_write = models.IntegerField(null=True)
1142
1143 # in seconds
1144 cpu_time_user = models.DecimalField(max_digits=8, decimal_places=2, null=True)
1145 cpu_time_system = models.DecimalField(max_digits=8, decimal_places=2, null=True)
1146
1147 sstate_result = models.IntegerField(choices=SSTATE_RESULT, default=SSTATE_NA)
1148 message = models.CharField(max_length=240)
1149 logfile = models.FilePathField(max_length=255, blank=True)
1150
1151 outcome_text = property(get_outcome_text)
1152 sstate_text = property(get_sstate_text)
1153
1154 def __unicode__(self):
1155 return "%d(%d) %s:%s" % (self.pk, self.build.pk, self.recipe.name, self.task_name)
1156
1157 class Meta:
1158 ordering = ('order', 'recipe' ,)
1159 unique_together = ('build', 'recipe', 'task_name', )
1160
1161
1162class Task_Dependency(models.Model):
1163 task = models.ForeignKey(Task, related_name='task_dependencies_task')
1164 depends_on = models.ForeignKey(Task, related_name='task_dependencies_depends')
1165
1166class Package(models.Model):
1167 search_allowed_fields = ['name', 'version', 'revision', 'recipe__name', 'recipe__version', 'recipe__license', 'recipe__layer_version__layer__name', 'recipe__layer_version__branch', 'recipe__layer_version__commit', 'recipe__layer_version__local_path', 'installed_name']
1168 build = models.ForeignKey('Build', null=True)
1169 recipe = models.ForeignKey('Recipe', null=True)
1170 name = models.CharField(max_length=100)
1171 installed_name = models.CharField(max_length=100, default='')
1172 version = models.CharField(max_length=100, blank=True)
1173 revision = models.CharField(max_length=32, blank=True)
1174 summary = models.TextField(blank=True)
1175 description = models.TextField(blank=True)
1176 size = models.IntegerField(default=0)
1177 installed_size = models.IntegerField(default=0)
1178 section = models.CharField(max_length=80, blank=True)
1179 license = models.CharField(max_length=80, blank=True)
1180
1181 @property
1182 def is_locale_package(self):
1183 """ Returns True if this package is identifiable as a locale package """
1184 if self.name.find('locale') != -1:
1185 return True
1186 return False
1187
1188 @property
1189 def is_packagegroup(self):
1190 """ Returns True is this package is identifiable as a packagegroup """
1191 if self.name.find('packagegroup') != -1:
1192 return True
1193 return False
1194
1195class CustomImagePackage(Package):
1196 # CustomImageRecipe fields to track pacakges appended,
1197 # included and excluded from a CustomImageRecipe
1198 recipe_includes = models.ManyToManyField('CustomImageRecipe',
1199 related_name='includes_set')
1200 recipe_excludes = models.ManyToManyField('CustomImageRecipe',
1201 related_name='excludes_set')
1202 recipe_appends = models.ManyToManyField('CustomImageRecipe',
1203 related_name='appends_set')
1204
1205
1206class Package_DependencyManager(models.Manager):
1207 use_for_related_fields = True
1208 TARGET_LATEST = "use-latest-target-for-target"
1209
1210 def get_queryset(self):
1211 return super(Package_DependencyManager, self).get_queryset().exclude(package_id = F('depends_on__id'))
1212
1213 def for_target_or_none(self, target):
1214 """ filter the dependencies to be displayed by the supplied target
1215 if no dependences are found for the target then try None as the target
1216 which will return the dependences calculated without the context of a
1217 target e.g. non image recipes.
1218
1219 returns: { size, packages }
1220 """
1221 package_dependencies = self.all_depends().order_by('depends_on__name')
1222
1223 if target is self.TARGET_LATEST:
1224 installed_deps =\
1225 package_dependencies.filter(~Q(target__target=None))
1226 else:
1227 installed_deps =\
1228 package_dependencies.filter(Q(target__target=target))
1229
1230 packages_list = None
1231 total_size = 0
1232
1233 # If we have installed depdencies for this package and target then use
1234 # these to display
1235 if installed_deps.count() > 0:
1236 packages_list = installed_deps
1237 total_size = installed_deps.aggregate(
1238 Sum('depends_on__size'))['depends_on__size__sum']
1239 else:
1240 new_list = []
1241 package_names = []
1242
1243 # Find dependencies for the package that we know about even if
1244 # it's not installed on a target e.g. from a non-image recipe
1245 for p in package_dependencies.filter(Q(target=None)):
1246 if p.depends_on.name in package_names:
1247 continue
1248 else:
1249 package_names.append(p.depends_on.name)
1250 new_list.append(p.pk)
1251 # while we're here we may as well total up the size to
1252 # avoid iterating again
1253 total_size += p.depends_on.size
1254
1255 # We want to return a queryset here for consistency so pick the
1256 # deps from the new_list
1257 packages_list = package_dependencies.filter(Q(pk__in=new_list))
1258
1259 return {'packages': packages_list,
1260 'size': total_size}
1261
1262 def all_depends(self):
1263 """ Returns just the depends packages and not any other dep_type
1264 Note that this is for any target
1265 """
1266 return self.filter(Q(dep_type=Package_Dependency.TYPE_RDEPENDS) |
1267 Q(dep_type=Package_Dependency.TYPE_TRDEPENDS))
1268
1269
1270class Package_Dependency(models.Model):
1271 TYPE_RDEPENDS = 0
1272 TYPE_TRDEPENDS = 1
1273 TYPE_RRECOMMENDS = 2
1274 TYPE_TRECOMMENDS = 3
1275 TYPE_RSUGGESTS = 4
1276 TYPE_RPROVIDES = 5
1277 TYPE_RREPLACES = 6
1278 TYPE_RCONFLICTS = 7
1279 ' TODO: bpackage should be changed to remove the DEPENDS_TYPE access '
1280 DEPENDS_TYPE = (
1281 (TYPE_RDEPENDS, "depends"),
1282 (TYPE_TRDEPENDS, "depends"),
1283 (TYPE_TRECOMMENDS, "recommends"),
1284 (TYPE_RRECOMMENDS, "recommends"),
1285 (TYPE_RSUGGESTS, "suggests"),
1286 (TYPE_RPROVIDES, "provides"),
1287 (TYPE_RREPLACES, "replaces"),
1288 (TYPE_RCONFLICTS, "conflicts"),
1289 )
1290 """ Indexed by dep_type, in view order, key for short name and help
1291 description which when viewed will be printf'd with the
1292 package name.
1293 """
1294 DEPENDS_DICT = {
1295 TYPE_RDEPENDS : ("depends", "%s is required to run %s"),
1296 TYPE_TRDEPENDS : ("depends", "%s is required to run %s"),
1297 TYPE_TRECOMMENDS : ("recommends", "%s extends the usability of %s"),
1298 TYPE_RRECOMMENDS : ("recommends", "%s extends the usability of %s"),
1299 TYPE_RSUGGESTS : ("suggests", "%s is suggested for installation with %s"),
1300 TYPE_RPROVIDES : ("provides", "%s is provided by %s"),
1301 TYPE_RREPLACES : ("replaces", "%s is replaced by %s"),
1302 TYPE_RCONFLICTS : ("conflicts", "%s conflicts with %s, which will not be installed if this package is not first removed"),
1303 }
1304
1305 package = models.ForeignKey(Package, related_name='package_dependencies_source')
1306 depends_on = models.ForeignKey(Package, related_name='package_dependencies_target') # soft dependency
1307 dep_type = models.IntegerField(choices=DEPENDS_TYPE)
1308 target = models.ForeignKey(Target, null=True)
1309 objects = Package_DependencyManager()
1310
1311class Target_Installed_Package(models.Model):
1312 target = models.ForeignKey(Target)
1313 package = models.ForeignKey(Package, related_name='buildtargetlist_package')
1314
1315
1316class Package_File(models.Model):
1317 package = models.ForeignKey(Package, related_name='buildfilelist_package')
1318 path = models.FilePathField(max_length=255, blank=True)
1319 size = models.IntegerField()
1320
1321
1322class Recipe(models.Model):
1323 search_allowed_fields = ['name', 'version', 'file_path', 'section',
1324 'summary', 'description', 'license',
1325 'layer_version__layer__name',
1326 'layer_version__branch', 'layer_version__commit',
1327 'layer_version__local_path',
1328 'layer_version__layer_source']
1329
1330 up_date = models.DateTimeField(null=True, default=None)
1331
1332 name = models.CharField(max_length=100, blank=True)
1333 version = models.CharField(max_length=100, blank=True)
1334 layer_version = models.ForeignKey('Layer_Version',
1335 related_name='recipe_layer_version')
1336 summary = models.TextField(blank=True)
1337 description = models.TextField(blank=True)
1338 section = models.CharField(max_length=100, blank=True)
1339 license = models.CharField(max_length=200, blank=True)
1340 homepage = models.URLField(blank=True)
1341 bugtracker = models.URLField(blank=True)
1342 file_path = models.FilePathField(max_length=255)
1343 pathflags = models.CharField(max_length=200, blank=True)
1344 is_image = models.BooleanField(default=False)
1345
1346 def __unicode__(self):
1347 return "Recipe " + self.name + ":" + self.version
1348
1349 def get_vcs_recipe_file_link_url(self):
1350 return self.layer_version.get_vcs_file_link_url(self.file_path)
1351
1352 def get_description_or_summary(self):
1353 if self.description:
1354 return self.description
1355 elif self.summary:
1356 return self.summary
1357 else:
1358 return ""
1359
1360 class Meta:
1361 unique_together = (("layer_version", "file_path", "pathflags"), )
1362
1363
1364class Recipe_DependencyManager(models.Manager):
1365 use_for_related_fields = True
1366
1367 def get_queryset(self):
1368 return super(Recipe_DependencyManager, self).get_queryset().exclude(recipe_id = F('depends_on__id'))
1369
1370class Provides(models.Model):
1371 name = models.CharField(max_length=100)
1372 recipe = models.ForeignKey(Recipe)
1373
1374class Recipe_Dependency(models.Model):
1375 TYPE_DEPENDS = 0
1376 TYPE_RDEPENDS = 1
1377
1378 DEPENDS_TYPE = (
1379 (TYPE_DEPENDS, "depends"),
1380 (TYPE_RDEPENDS, "rdepends"),
1381 )
1382 recipe = models.ForeignKey(Recipe, related_name='r_dependencies_recipe')
1383 depends_on = models.ForeignKey(Recipe, related_name='r_dependencies_depends')
1384 via = models.ForeignKey(Provides, null=True, default=None)
1385 dep_type = models.IntegerField(choices=DEPENDS_TYPE)
1386 objects = Recipe_DependencyManager()
1387
1388
1389class Machine(models.Model):
1390 search_allowed_fields = ["name", "description", "layer_version__layer__name"]
1391 up_date = models.DateTimeField(null = True, default = None)
1392
1393 layer_version = models.ForeignKey('Layer_Version')
1394 name = models.CharField(max_length=255)
1395 description = models.CharField(max_length=255)
1396
1397 def get_vcs_machine_file_link_url(self):
1398 path = 'conf/machine/'+self.name+'.conf'
1399
1400 return self.layer_version.get_vcs_file_link_url(path)
1401
1402 def __unicode__(self):
1403 return "Machine " + self.name + "(" + self.description + ")"
1404
1405
1406
1407
1408
1409class BitbakeVersion(models.Model):
1410
1411 name = models.CharField(max_length=32, unique = True)
1412 giturl = GitURLField()
1413 branch = models.CharField(max_length=32)
1414 dirpath = models.CharField(max_length=255)
1415
1416 def __unicode__(self):
1417 return "%s (Branch: %s)" % (self.name, self.branch)
1418
1419
1420class Release(models.Model):
1421 """ A release is a project template, used to pre-populate Project settings with a configuration set """
1422 name = models.CharField(max_length=32, unique = True)
1423 description = models.CharField(max_length=255)
1424 bitbake_version = models.ForeignKey(BitbakeVersion)
1425 branch_name = models.CharField(max_length=50, default = "")
1426 helptext = models.TextField(null=True)
1427
1428 def __unicode__(self):
1429 return "%s (%s)" % (self.name, self.branch_name)
1430
1431 def __str__(self):
1432 return self.name
1433
1434class ReleaseDefaultLayer(models.Model):
1435 release = models.ForeignKey(Release)
1436 layer_name = models.CharField(max_length=100, default="")
1437
1438
1439class LayerSource(object):
1440 """ Where the layer metadata came from """
1441 TYPE_LOCAL = 0
1442 TYPE_LAYERINDEX = 1
1443 TYPE_IMPORTED = 2
1444 TYPE_BUILD = 3
1445
1446 SOURCE_TYPE = (
1447 (TYPE_LOCAL, "local"),
1448 (TYPE_LAYERINDEX, "layerindex"),
1449 (TYPE_IMPORTED, "imported"),
1450 (TYPE_BUILD, "build"),
1451 )
1452
1453 def types_dict():
1454 """ Turn the TYPES enums into a simple dictionary """
1455 dictionary = {}
1456 for key in LayerSource.__dict__:
1457 if "TYPE" in key:
1458 dictionary[key] = getattr(LayerSource, key)
1459 return dictionary
1460
1461
1462class Layer(models.Model):
1463
1464 up_date = models.DateTimeField(null=True, default=timezone.now)
1465
1466 name = models.CharField(max_length=100)
1467 layer_index_url = models.URLField()
1468 vcs_url = GitURLField(default=None, null=True)
1469 local_source_dir = models.TextField(null=True, default=None)
1470 vcs_web_url = models.URLField(null=True, default=None)
1471 vcs_web_tree_base_url = models.URLField(null=True, default=None)
1472 vcs_web_file_base_url = models.URLField(null=True, default=None)
1473
1474 summary = models.TextField(help_text='One-line description of the layer',
1475 null=True, default=None)
1476 description = models.TextField(null=True, default=None)
1477
1478 def __unicode__(self):
1479 return "%s / %s " % (self.name, self.summary)
1480
1481
1482class Layer_Version(models.Model):
1483 """
1484 A Layer_Version either belongs to a single project or no project
1485 """
1486 search_allowed_fields = ["layer__name", "layer__summary",
1487 "layer__description", "layer__vcs_url",
1488 "dirpath", "release__name", "commit", "branch"]
1489
1490 build = models.ForeignKey(Build, related_name='layer_version_build',
1491 default=None, null=True)
1492
1493 layer = models.ForeignKey(Layer, related_name='layer_version_layer')
1494
1495 layer_source = models.IntegerField(choices=LayerSource.SOURCE_TYPE,
1496 default=0)
1497
1498 up_date = models.DateTimeField(null=True, default=timezone.now)
1499
1500 # To which metadata release does this layer version belong to
1501 release = models.ForeignKey(Release, null=True, default=None)
1502
1503 branch = models.CharField(max_length=80)
1504 commit = models.CharField(max_length=100)
1505 # If the layer is in a subdir
1506 dirpath = models.CharField(max_length=255, null=True, default=None)
1507
1508 # if -1, this is a default layer
1509 priority = models.IntegerField(default=0)
1510
1511 # where this layer exists on the filesystem
1512 local_path = models.FilePathField(max_length=1024, default="/")
1513
1514 # Set if this layer is restricted to a particular project
1515 project = models.ForeignKey('Project', null=True, default=None)
1516
1517 # code lifted, with adaptations, from the layerindex-web application
1518 # https://git.yoctoproject.org/cgit/cgit.cgi/layerindex-web/
1519 def _handle_url_path(self, base_url, path):
1520 import re, posixpath
1521 if base_url:
1522 if self.dirpath:
1523 if path:
1524 extra_path = self.dirpath + '/' + path
1525 # Normalise out ../ in path for usage URL
1526 extra_path = posixpath.normpath(extra_path)
1527 # Minor workaround to handle case where subdirectory has been added between branches
1528 # (should probably support usage URL per branch to handle this... sigh...)
1529 if extra_path.startswith('../'):
1530 extra_path = extra_path[3:]
1531 else:
1532 extra_path = self.dirpath
1533 else:
1534 extra_path = path
1535 branchname = self.release.name
1536 url = base_url.replace('%branch%', branchname)
1537
1538 # If there's a % in the path (e.g. a wildcard bbappend) we need to encode it
1539 if extra_path:
1540 extra_path = extra_path.replace('%', '%25')
1541
1542 if '%path%' in base_url:
1543 if extra_path:
1544 url = re.sub(r'\[([^\]]*%path%[^\]]*)\]', '\\1', url)
1545 else:
1546 url = re.sub(r'\[([^\]]*%path%[^\]]*)\]', '', url)
1547 return url.replace('%path%', extra_path)
1548 else:
1549 return url + extra_path
1550 return None
1551
1552 def get_vcs_link_url(self):
1553 if self.layer.vcs_web_url is None:
1554 return None
1555 return self.layer.vcs_web_url
1556
1557 def get_vcs_file_link_url(self, file_path=""):
1558 if self.layer.vcs_web_file_base_url is None:
1559 return None
1560 return self._handle_url_path(self.layer.vcs_web_file_base_url,
1561 file_path)
1562
1563 def get_vcs_dirpath_link_url(self):
1564 if self.layer.vcs_web_tree_base_url is None:
1565 return None
1566 return self._handle_url_path(self.layer.vcs_web_tree_base_url, '')
1567
1568 def get_vcs_reference(self):
1569 if self.commit is not None and len(self.commit) > 0:
1570 return self.commit
1571 if self.branch is not None and len(self.branch) > 0:
1572 return self.branch
1573 if self.release is not None:
1574 return self.release.name
1575 return 'N/A'
1576
1577 def get_detailspage_url(self, project_id=None):
1578 """ returns the url to the layer details page uses own project
1579 field if project_id is not specified """
1580
1581 if project_id is None:
1582 project_id = self.project.pk
1583
1584 return reverse('layerdetails', args=(project_id, self.pk))
1585
1586 def get_alldeps(self, project_id):
1587 """Get full list of unique layer dependencies."""
1588 def gen_layerdeps(lver, project, depth):
1589 if depth == 0:
1590 return
1591 for ldep in lver.dependencies.all():
1592 yield ldep.depends_on
1593 # get next level of deps recursively calling gen_layerdeps
1594 for subdep in gen_layerdeps(ldep.depends_on, project, depth-1):
1595 yield subdep
1596
1597 project = Project.objects.get(pk=project_id)
1598 result = []
1599 projectlvers = [player.layercommit for player in
1600 project.projectlayer_set.all()]
1601 # protect against infinite layer dependency loops
1602 maxdepth = 20
1603 for dep in gen_layerdeps(self, project, maxdepth):
1604 # filter out duplicates and layers already belonging to the project
1605 if dep not in result + projectlvers:
1606 result.append(dep)
1607
1608 return sorted(result, key=lambda x: x.layer.name)
1609
1610 def __unicode__(self):
1611 return ("id %d belongs to layer: %s" % (self.pk, self.layer.name))
1612
1613 def __str__(self):
1614 if self.release:
1615 release = self.release.name
1616 else:
1617 release = "No release set"
1618
1619 return "%d %s (%s)" % (self.pk, self.layer.name, release)
1620
1621
1622class LayerVersionDependency(models.Model):
1623
1624 layer_version = models.ForeignKey(Layer_Version,
1625 related_name="dependencies")
1626 depends_on = models.ForeignKey(Layer_Version,
1627 related_name="dependees")
1628
1629class ProjectLayer(models.Model):
1630 project = models.ForeignKey(Project)
1631 layercommit = models.ForeignKey(Layer_Version, null=True)
1632 optional = models.BooleanField(default = True)
1633
1634 def __unicode__(self):
1635 return "%s, %s" % (self.project.name, self.layercommit)
1636
1637 class Meta:
1638 unique_together = (("project", "layercommit"),)
1639
1640class CustomImageRecipe(Recipe):
1641
1642 # CustomImageRecipe's belong to layers called:
1643 LAYER_NAME = "toaster-custom-images"
1644
1645 search_allowed_fields = ['name']
1646 base_recipe = models.ForeignKey(Recipe, related_name='based_on_recipe')
1647 project = models.ForeignKey(Project)
1648 last_updated = models.DateTimeField(null=True, default=None)
1649
1650 def get_last_successful_built_target(self):
1651 """ Return the last successful built target object if one exists
1652 otherwise return None """
1653 return Target.objects.filter(Q(build__outcome=Build.SUCCEEDED) &
1654 Q(build__project=self.project) &
1655 Q(target=self.name)).last()
1656
1657 def update_package_list(self):
1658 """ Update the package list from the last good build of this
1659 CustomImageRecipe
1660 """
1661 # Check if we're aldready up-to-date or not
1662 target = self.get_last_successful_built_target()
1663 if target == None:
1664 # So we've never actually built this Custom recipe but what about
1665 # the recipe it's based on?
1666 target = \
1667 Target.objects.filter(Q(build__outcome=Build.SUCCEEDED) &
1668 Q(build__project=self.project) &
1669 Q(target=self.base_recipe.name)).last()
1670 if target == None:
1671 return
1672
1673 if target.build.completed_on == self.last_updated:
1674 return
1675
1676 self.includes_set.clear()
1677
1678 excludes_list = self.excludes_set.values_list('name', flat=True)
1679 appends_list = self.appends_set.values_list('name', flat=True)
1680
1681 built_packages_list = \
1682 target.target_installed_package_set.values_list('package__name',
1683 flat=True)
1684 for built_package in built_packages_list:
1685 # Is the built package in the custom packages list?
1686 if built_package in excludes_list:
1687 continue
1688
1689 if built_package in appends_list:
1690 continue
1691
1692 cust_img_p = \
1693 CustomImagePackage.objects.get(name=built_package)
1694 self.includes_set.add(cust_img_p)
1695
1696
1697 self.last_updated = target.build.completed_on
1698 self.save()
1699
1700 def get_all_packages(self):
1701 """Get the included packages and any appended packages"""
1702 self.update_package_list()
1703
1704 return CustomImagePackage.objects.filter((Q(recipe_appends=self) |
1705 Q(recipe_includes=self)) &
1706 ~Q(recipe_excludes=self))
1707
1708 def get_base_recipe_file(self):
1709 """Get the base recipe file path if it exists on the file system"""
1710 path_schema_one = "%s/%s" % (self.base_recipe.layer_version.local_path,
1711 self.base_recipe.file_path)
1712
1713 path_schema_two = self.base_recipe.file_path
1714
1715 path_schema_three = "%s/%s" % (self.base_recipe.layer_version.layer.local_source_dir,
1716 self.base_recipe.file_path)
1717
1718 if os.path.exists(path_schema_one):
1719 return path_schema_one
1720
1721 # The path may now be the full path if the recipe has been built
1722 if os.path.exists(path_schema_two):
1723 return path_schema_two
1724
1725 # Or a local path if all layers are local
1726 if os.path.exists(path_schema_three):
1727 return path_schema_three
1728
1729 return None
1730
1731 def generate_recipe_file_contents(self):
1732 """Generate the contents for the recipe file."""
1733 # If we have no excluded packages we only need to _append
1734 if self.excludes_set.count() == 0:
1735 packages_conf = "IMAGE_INSTALL_append = \" "
1736
1737 for pkg in self.appends_set.all():
1738 packages_conf += pkg.name+' '
1739 else:
1740 packages_conf = "IMAGE_FEATURES =\"\"\nIMAGE_INSTALL = \""
1741 # We add all the known packages to be built by this recipe apart
1742 # from locale packages which are are controlled with IMAGE_LINGUAS.
1743 for pkg in self.get_all_packages().exclude(
1744 name__icontains="locale"):
1745 packages_conf += pkg.name+' '
1746
1747 packages_conf += "\""
1748
1749 base_recipe_path = self.get_base_recipe_file()
1750 if base_recipe_path:
1751 base_recipe = open(base_recipe_path, 'r').read()
1752 else:
1753 # Pass back None to trigger error message to user
1754 return None
1755
1756 # Add a special case for when the recipe we have based a custom image
1757 # recipe on requires another recipe.
1758 # For example:
1759 # "require core-image-minimal.bb" is changed to:
1760 # "require recipes-core/images/core-image-minimal.bb"
1761
1762 req_search = re.search(r'(require\s+)(.+\.bb\s*$)',
1763 base_recipe,
1764 re.MULTILINE)
1765 if req_search:
1766 require_filename = req_search.group(2).strip()
1767
1768 corrected_location = Recipe.objects.filter(
1769 Q(layer_version=self.base_recipe.layer_version) &
1770 Q(file_path__icontains=require_filename)).last().file_path
1771
1772 new_require_line = "require %s" % corrected_location
1773
1774 base_recipe = base_recipe.replace(req_search.group(0),
1775 new_require_line)
1776
1777 info = {
1778 "date": timezone.now().strftime("%Y-%m-%d %H:%M:%S"),
1779 "base_recipe": base_recipe,
1780 "recipe_name": self.name,
1781 "base_recipe_name": self.base_recipe.name,
1782 "license": self.license,
1783 "summary": self.summary,
1784 "description": self.description,
1785 "packages_conf": packages_conf.strip()
1786 }
1787
1788 recipe_contents = ("# Original recipe %(base_recipe_name)s \n"
1789 "%(base_recipe)s\n\n"
1790 "# Recipe %(recipe_name)s \n"
1791 "# Customisation Generated by Toaster on %(date)s\n"
1792 "SUMMARY = \"%(summary)s\"\n"
1793 "DESCRIPTION = \"%(description)s\"\n"
1794 "LICENSE = \"%(license)s\"\n"
1795 "%(packages_conf)s") % info
1796
1797 return recipe_contents
1798
1799class ProjectVariable(models.Model):
1800 project = models.ForeignKey(Project)
1801 name = models.CharField(max_length=100)
1802 value = models.TextField(blank = True)
1803
1804class Variable(models.Model):
1805 search_allowed_fields = ['variable_name', 'variable_value',
1806 'vhistory__file_name', "description"]
1807 build = models.ForeignKey(Build, related_name='variable_build')
1808 variable_name = models.CharField(max_length=100)
1809 variable_value = models.TextField(blank=True)
1810 changed = models.BooleanField(default=False)
1811 human_readable_name = models.CharField(max_length=200)
1812 description = models.TextField(blank=True)
1813
1814class VariableHistory(models.Model):
1815 variable = models.ForeignKey(Variable, related_name='vhistory')
1816 value = models.TextField(blank=True)
1817 file_name = models.FilePathField(max_length=255)
1818 line_number = models.IntegerField(null=True)
1819 operation = models.CharField(max_length=64)
1820
1821class HelpText(models.Model):
1822 VARIABLE = 0
1823 HELPTEXT_AREA = ((VARIABLE, 'variable'), )
1824
1825 build = models.ForeignKey(Build, related_name='helptext_build')
1826 area = models.IntegerField(choices=HELPTEXT_AREA)
1827 key = models.CharField(max_length=100)
1828 text = models.TextField()
1829
1830class LogMessage(models.Model):
1831 EXCEPTION = -1 # used to signal self-toaster-exceptions
1832 INFO = 0
1833 WARNING = 1
1834 ERROR = 2
1835 CRITICAL = 3
1836
1837 LOG_LEVEL = (
1838 (INFO, "info"),
1839 (WARNING, "warn"),
1840 (ERROR, "error"),
1841 (CRITICAL, "critical"),
1842 (EXCEPTION, "toaster exception")
1843 )
1844
1845 build = models.ForeignKey(Build)
1846 task = models.ForeignKey(Task, blank = True, null=True)
1847 level = models.IntegerField(choices=LOG_LEVEL, default=INFO)
1848 message = models.TextField(blank=True, null=True)
1849 pathname = models.FilePathField(max_length=255, blank=True)
1850 lineno = models.IntegerField(null=True)
1851
1852 def __str__(self):
1853 return force_bytes('%s %s %s' % (self.get_level_display(), self.message, self.build))
1854
1855def invalidate_cache(**kwargs):
1856 from django.core.cache import cache
1857 try:
1858 cache.clear()
1859 except Exception as e:
1860 logger.warning("Problem with cache backend: Failed to clear cache: %s" % e)
1861
1862def signal_runbuilds():
1863 """Send SIGUSR1 to runbuilds process"""
1864 try:
1865 with open(os.path.join(os.getenv('BUILDDIR', '.'),
1866 '.runbuilds.pid')) as pidf:
1867 os.kill(int(pidf.read()), SIGUSR1)
1868 except FileNotFoundError:
1869 logger.info("Stopping existing runbuilds: no current process found")
1870
1871class Distro(models.Model):
1872 search_allowed_fields = ["name", "description", "layer_version__layer__name"]
1873 up_date = models.DateTimeField(null = True, default = None)
1874
1875 layer_version = models.ForeignKey('Layer_Version')
1876 name = models.CharField(max_length=255)
1877 description = models.CharField(max_length=255)
1878
1879 def get_vcs_distro_file_link_url(self):
1880 path = 'conf/distro/%s.conf' % self.name
1881 return self.layer_version.get_vcs_file_link_url(path)
1882
1883 def __unicode__(self):
1884 return "Distro " + self.name + "(" + self.description + ")"
1885
1886django.db.models.signals.post_save.connect(invalidate_cache)
1887django.db.models.signals.post_delete.connect(invalidate_cache)
1888django.db.models.signals.m2m_changed.connect(invalidate_cache)