blob: 4b77e8fda8c9e475f75551ba8df3d5ee7685aedd [file] [log] [blame]
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001#
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
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050022from __future__ import unicode_literals
23
Patrick Williamsc0f7c042017-02-23 20:41:17 -060024from django.db import models, IntegrityError, DataError
25from django.db.models import F, Q, Sum, Count
Patrick Williamsc124f4f2015-09-15 14:41:29 -050026from django.utils import timezone
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050027from django.utils.encoding import force_bytes
Patrick Williamsc124f4f2015-09-15 14:41:29 -050028
29from django.core.urlresolvers import reverse
30
31from django.core import validators
32from django.conf import settings
33import django.db.models.signals
34
Patrick Williamsc0f7c042017-02-23 20:41:17 -060035import sys
36import os
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050037import re
38import itertools
Patrick Williamsc0f7c042017-02-23 20:41:17 -060039from signal import SIGUSR1
Patrick Williamsc124f4f2015-09-15 14:41:29 -050040
Brad Bishop6e60e8b2018-02-01 10:27:11 -050041
Patrick Williamsc124f4f2015-09-15 14:41:29 -050042import logging
43logger = logging.getLogger("toaster")
44
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050045if '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
Patrick Williamsc0f7c042017-02-23 20:41:17 -060083 except (IntegrityError, DataError):
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050084 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
Patrick Williamsc124f4f2015-09-15 14:41:29 -050094
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)
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600108 for i in range(len(r.validators)):
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500109 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
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600122
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500123class ProjectManager(models.Manager):
124 def create_project(self, name, release):
125 if release is not None:
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600126 prj = self.model(name=name,
127 bitbake_version=release.bitbake_version,
128 release=release)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500129 else:
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600130 prj = self.model(name=name,
131 bitbake_version=None,
132 release=None)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500133
134 prj.save()
135
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600136 for defaultconf in ToasterSetting.objects.filter(
137 name__startswith="DEFCONF_"):
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500138 name = defaultconf.name[8:]
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600139 ProjectVariable.objects.create(project=prj,
140 name=name,
141 value=defaultconf.value)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500142
143 if release is None:
144 return prj
145
146 for rdl in release.releasedefaultlayer_set.all():
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600147 lv = Layer_Version.objects.filter(
148 layer__name=rdl.layer_name,
149 release=release).first()
150
151 if lv:
152 ProjectLayer.objects.create(project=prj,
153 layercommit=lv,
154 optional=False)
155 else:
156 logger.warning("Default project layer %s not found" %
157 rdl.layer_name)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500158
159 return prj
160
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500161 # return single object with is_default = True
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500162 def get_or_create_default_project(self):
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600163 projects = super(ProjectManager, self).filter(is_default=True)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500164
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500165 if len(projects) > 1:
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500166 raise Exception('Inconsistent project data: multiple ' +
167 'default projects (i.e. with is_default=True)')
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500168 elif len(projects) < 1:
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500169 options = {
170 'name': 'Command line builds',
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600171 'short_description':
172 'Project for builds started outside Toaster',
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500173 'is_default': True
174 }
175 project = Project.objects.create(**options)
176 project.save()
177
178 return project
179 else:
180 return projects[0]
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500181
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500182
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500183class Project(models.Model):
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500184 search_allowed_fields = ['name', 'short_description', 'release__name',
185 'release__branch_name']
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500186 name = models.CharField(max_length=100)
187 short_description = models.CharField(max_length=50, blank=True)
188 bitbake_version = models.ForeignKey('BitbakeVersion', null=True)
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500189 release = models.ForeignKey("Release", null=True)
190 created = models.DateTimeField(auto_now_add=True)
191 updated = models.DateTimeField(auto_now=True)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500192 # This is a horrible hack; since Toaster has no "User" model available when
193 # running in interactive mode, we can't reference the field here directly
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500194 # Instead, we keep a possible null reference to the User id,
195 # as not to force
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500196 # hard links to possibly missing models
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500197 user_id = models.IntegerField(null=True)
198 objects = ProjectManager()
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500199
200 # set to True for the project which is the default container
201 # for builds initiated by the command line etc.
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500202 is_default= models.BooleanField(default=False)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500203
204 def __unicode__(self):
205 return "%s (Release %s, BBV %s)" % (self.name, self.release, self.bitbake_version)
206
207 def get_current_machine_name(self):
208 try:
209 return self.projectvariable_set.get(name="MACHINE").value
210 except (ProjectVariable.DoesNotExist,IndexError):
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500211 return None;
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500212
213 def get_number_of_builds(self):
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500214 """Return the number of builds which have ended"""
215
216 return self.build_set.exclude(
217 Q(outcome=Build.IN_PROGRESS) |
218 Q(outcome=Build.CANCELLED)
219 ).count()
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500220
221 def get_last_build_id(self):
222 try:
223 return Build.objects.filter( project = self.id ).order_by('-completed_on')[0].id
224 except (Build.DoesNotExist,IndexError):
225 return( -1 )
226
227 def get_last_outcome(self):
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500228 build_id = self.get_last_build_id()
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500229 if (-1 == build_id):
230 return( "" )
231 try:
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500232 return Build.objects.filter( id = build_id )[ 0 ].outcome
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500233 except (Build.DoesNotExist,IndexError):
234 return( "not_found" )
235
236 def get_last_target(self):
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500237 build_id = self.get_last_build_id()
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500238 if (-1 == build_id):
239 return( "" )
240 try:
241 return Target.objects.filter(build = build_id)[0].target
242 except (Target.DoesNotExist,IndexError):
243 return( "not_found" )
244
245 def get_last_errors(self):
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500246 build_id = self.get_last_build_id()
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500247 if (-1 == build_id):
248 return( 0 )
249 try:
250 return Build.objects.filter(id = build_id)[ 0 ].errors.count()
251 except (Build.DoesNotExist,IndexError):
252 return( "not_found" )
253
254 def get_last_warnings(self):
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500255 build_id = self.get_last_build_id()
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500256 if (-1 == build_id):
257 return( 0 )
258 try:
259 return Build.objects.filter(id = build_id)[ 0 ].warnings.count()
260 except (Build.DoesNotExist,IndexError):
261 return( "not_found" )
262
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500263 def get_last_build_extensions(self):
264 """
265 Get list of file name extensions for images produced by the most
266 recent build
267 """
268 last_build = Build.objects.get(pk = self.get_last_build_id())
269 return last_build.get_image_file_extensions()
270
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500271 def get_last_imgfiles(self):
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500272 build_id = self.get_last_build_id()
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500273 if (-1 == build_id):
274 return( "" )
275 try:
276 return Variable.objects.filter(build = build_id, variable_name = "IMAGE_FSTYPES")[ 0 ].variable_value
277 except (Variable.DoesNotExist,IndexError):
278 return( "not_found" )
279
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500280 def get_all_compatible_layer_versions(self):
281 """ Returns Queryset of all Layer_Versions which are compatible with
282 this project"""
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500283 queryset = None
284
285 # guard on release, as it can be null
286 if self.release:
287 queryset = Layer_Version.objects.filter(
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600288 (Q(release=self.release) &
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500289 Q(build=None) &
290 Q(project=None)) |
291 Q(project=self))
292 else:
293 queryset = Layer_Version.objects.none()
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500294
295 return queryset
296
297 def get_project_layer_versions(self, pk=False):
298 """ Returns the Layer_Versions currently added to this project """
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500299 layer_versions = self.projectlayer_set.all().values_list('layercommit',
300 flat=True)
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500301
302 if pk is False:
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500303 return Layer_Version.objects.filter(pk__in=layer_versions)
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500304 else:
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500305 return layer_versions
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500306
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500307
308 def get_available_machines(self):
309 """ Returns QuerySet of all Machines which are provided by the
310 Layers currently added to the Project """
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500311 queryset = Machine.objects.filter(
312 layer_version__in=self.get_project_layer_versions())
313
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500314 return queryset
315
316 def get_all_compatible_machines(self):
317 """ Returns QuerySet of all the compatible machines available to the
318 project including ones from Layers not currently added """
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500319 queryset = Machine.objects.filter(
320 layer_version__in=self.get_all_compatible_layer_versions())
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500321
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500322 return queryset
323
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500324 def get_available_distros(self):
325 """ Returns QuerySet of all Distros which are provided by the
326 Layers currently added to the Project """
327 queryset = Distro.objects.filter(
328 layer_version__in=self.get_project_layer_versions())
329
330 return queryset
331
332 def get_all_compatible_distros(self):
333 """ Returns QuerySet of all the compatible Wind River distros available to the
334 project including ones from Layers not currently added """
335 queryset = Distro.objects.filter(
336 layer_version__in=self.get_all_compatible_layer_versions())
337
338 return queryset
339
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500340 def get_available_recipes(self):
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500341 """ Returns QuerySet of all the recipes that are provided by layers
342 added to this project """
343 queryset = Recipe.objects.filter(
344 layer_version__in=self.get_project_layer_versions())
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500345
346 return queryset
347
348 def get_all_compatible_recipes(self):
349 """ Returns QuerySet of all the compatible Recipes available to the
350 project including ones from Layers not currently added """
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500351 queryset = Recipe.objects.filter(
352 layer_version__in=self.get_all_compatible_layer_versions()).exclude(name__exact='')
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500353
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500354 return queryset
355
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500356 def schedule_build(self):
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500357
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500358 from bldcontrol.models import BuildRequest, BRTarget, BRLayer
359 from bldcontrol.models import BRBitbake, BRVariable
360
361 try:
362 now = timezone.now()
363 build = Build.objects.create(project=self,
364 completed_on=now,
365 started_on=now)
366
367 br = BuildRequest.objects.create(project=self,
368 state=BuildRequest.REQ_QUEUED,
369 build=build)
370 BRBitbake.objects.create(req=br,
371 giturl=self.bitbake_version.giturl,
372 commit=self.bitbake_version.branch,
373 dirpath=self.bitbake_version.dirpath)
374
375 for t in self.projecttarget_set.all():
376 BRTarget.objects.create(req=br, target=t.target, task=t.task)
377 Target.objects.create(build=br.build, target=t.target,
378 task=t.task)
379 # If we're about to build a custom image recipe make sure
380 # that layer is currently in the project before we create the
381 # BRLayer objects
382 customrecipe = CustomImageRecipe.objects.filter(
383 name=t.target,
384 project=self).first()
385 if customrecipe:
386 ProjectLayer.objects.get_or_create(
387 project=self,
388 layercommit=customrecipe.layer_version,
389 optional=False)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500390
391 for l in self.projectlayer_set.all().order_by("pk"):
392 commit = l.layercommit.get_vcs_reference()
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500393 logger.debug("Adding layer to build %s" %
394 l.layercommit.layer.name)
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600395 BRLayer.objects.create(
396 req=br,
397 name=l.layercommit.layer.name,
398 giturl=l.layercommit.layer.vcs_url,
399 commit=commit,
400 dirpath=l.layercommit.dirpath,
401 layer_version=l.layercommit,
402 local_source_dir=l.layercommit.layer.local_source_dir
403 )
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500404
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500405 for v in self.projectvariable_set.all():
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500406 BRVariable.objects.create(req=br, name=v.name, value=v.value)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500407
408 try:
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500409 br.build.machine = self.projectvariable_set.get(
410 name='MACHINE').value
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500411 br.build.save()
412 except ProjectVariable.DoesNotExist:
413 pass
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500414
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500415 br.save()
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600416 signal_runbuilds()
417
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500418 except Exception:
419 # revert the build request creation since we're not done cleanly
420 br.delete()
421 raise
422 return br
423
424class Build(models.Model):
425 SUCCEEDED = 0
426 FAILED = 1
427 IN_PROGRESS = 2
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500428 CANCELLED = 3
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500429
430 BUILD_OUTCOME = (
431 (SUCCEEDED, 'Succeeded'),
432 (FAILED, 'Failed'),
433 (IN_PROGRESS, 'In Progress'),
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500434 (CANCELLED, 'Cancelled'),
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500435 )
436
437 search_allowed_fields = ['machine', 'cooker_log_path', "target__target", "target__target_image_file__file_name"]
438
439 project = models.ForeignKey(Project) # must have a project
440 machine = models.CharField(max_length=100)
441 distro = models.CharField(max_length=100)
442 distro_version = models.CharField(max_length=100)
443 started_on = models.DateTimeField()
444 completed_on = models.DateTimeField()
445 outcome = models.IntegerField(choices=BUILD_OUTCOME, default=IN_PROGRESS)
446 cooker_log_path = models.CharField(max_length=500)
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600447 build_name = models.CharField(max_length=100, default='')
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500448 bitbake_version = models.CharField(max_length=50)
449
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600450 # number of recipes to parse for this build
451 recipes_to_parse = models.IntegerField(default=1)
452
453 # number of recipes parsed so far for this build
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500454 recipes_parsed = models.IntegerField(default=1)
455
456 # number of repos to clone for this build
457 repos_to_clone = models.IntegerField(default=1)
458
459 # number of repos cloned so far for this build (default off)
460 repos_cloned = models.IntegerField(default=1)
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600461
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500462 @staticmethod
463 def get_recent(project=None):
464 """
465 Return recent builds as a list; if project is set, only return
466 builds for that project
467 """
468
469 builds = Build.objects.all()
470
471 if project:
472 builds = builds.filter(project=project)
473
474 finished_criteria = \
475 Q(outcome=Build.SUCCEEDED) | \
476 Q(outcome=Build.FAILED) | \
477 Q(outcome=Build.CANCELLED)
478
479 recent_builds = list(itertools.chain(
480 builds.filter(outcome=Build.IN_PROGRESS).order_by("-started_on"),
481 builds.filter(finished_criteria).order_by("-completed_on")[:3]
482 ))
483
484 # add percentage done property to each build; this is used
485 # to show build progress in mrb_section.html
486 for build in recent_builds:
487 build.percentDone = build.completeper()
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600488 build.outcomeText = build.get_outcome_text()
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500489
490 return recent_builds
491
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600492 def started(self):
493 """
494 As build variables are only added for a build when its BuildStarted event
495 is received, a build with no build variables is counted as
496 "in preparation" and not properly started yet. This method
497 will return False if a build has no build variables (it never properly
498 started), or True otherwise.
499
500 Note that this is a temporary workaround for the fact that we don't
501 have a fine-grained state variable on a build which would allow us
502 to record "in progress" (BuildStarted received) vs. "in preparation".
503 """
504 variables = Variable.objects.filter(build=self)
505 return len(variables) > 0
506
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500507 def completeper(self):
508 tf = Task.objects.filter(build = self)
509 tfc = tf.count()
510 if tfc > 0:
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500511 completeper = tf.exclude(outcome=Task.OUTCOME_NA).count()*100 // tfc
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500512 else:
513 completeper = 0
514 return completeper
515
516 def eta(self):
517 eta = timezone.now()
518 completeper = self.completeper()
519 if self.completeper() > 0:
520 eta += ((eta - self.started_on)*(100-completeper))/completeper
521 return eta
522
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600523 def has_images(self):
524 """
525 Returns True if at least one of the targets for this build has an
526 image file associated with it, False otherwise
527 """
528 targets = Target.objects.filter(build_id=self.id)
529 has_images = False
530 for target in targets:
531 if target.has_images():
532 has_images = True
533 break
534 return has_images
535
536 def has_image_recipes(self):
537 """
538 Returns True if a build has any targets which were built from
539 image recipes.
540 """
541 image_recipes = self.get_image_recipes()
542 return len(image_recipes) > 0
543
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500544 def get_image_file_extensions(self):
545 """
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600546 Get string of file name extensions for images produced by this build;
547 note that this is the actual list of extensions stored on Target objects
548 for this build, and not the value of IMAGE_FSTYPES.
549
550 Returns comma-separated string, e.g. "vmdk, ext4"
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500551 """
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500552 extensions = []
553
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600554 targets = Target.objects.filter(build_id = self.id)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500555 for target in targets:
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600556 if not target.is_image:
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500557 continue
558
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600559 target_image_files = Target_Image_File.objects.filter(
560 target_id=target.id)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500561
562 for target_image_file in target_image_files:
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600563 extensions.append(target_image_file.suffix)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500564
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600565 extensions = list(set(extensions))
566 extensions.sort()
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500567
568 return ', '.join(extensions)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500569
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600570 def get_image_fstypes(self):
571 """
572 Get the IMAGE_FSTYPES variable value for this build as a de-duplicated
573 list of image file suffixes.
574 """
575 image_fstypes = Variable.objects.get(
576 build=self, variable_name='IMAGE_FSTYPES').variable_value
577 return list(set(re.split(r' {1,}', image_fstypes)))
578
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500579 def get_sorted_target_list(self):
580 tgts = Target.objects.filter(build_id = self.id).order_by( 'target' );
581 return( tgts );
582
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500583 def get_recipes(self):
584 """
585 Get the recipes related to this build;
586 note that the related layer versions and layers are also prefetched
587 by this query, as this queryset can be sorted by these objects in the
588 build recipes view; prefetching them here removes the need
589 for another query in that view
590 """
591 layer_versions = Layer_Version.objects.filter(build=self)
592 criteria = Q(layer_version__id__in=layer_versions)
593 return Recipe.objects.filter(criteria) \
594 .select_related('layer_version', 'layer_version__layer')
595
596 def get_image_recipes(self):
597 """
598 Returns a list of image Recipes (custom and built-in) related to this
599 build, sorted by name; note that this has to be done in two steps, as
600 there's no way to get all the custom image recipes and image recipes
601 in one query
602 """
603 custom_image_recipes = self.get_custom_image_recipes()
604 custom_image_recipe_names = custom_image_recipes.values_list('name', flat=True)
605
606 not_custom_image_recipes = ~Q(name__in=custom_image_recipe_names) & \
607 Q(is_image=True)
608
609 built_image_recipes = self.get_recipes().filter(not_custom_image_recipes)
610
611 # append to the custom image recipes and sort
612 customisable_image_recipes = list(
613 itertools.chain(custom_image_recipes, built_image_recipes)
614 )
615
616 return sorted(customisable_image_recipes, key=lambda recipe: recipe.name)
617
618 def get_custom_image_recipes(self):
619 """
620 Returns a queryset of CustomImageRecipes related to this build,
621 sorted by name
622 """
623 built_recipe_names = self.get_recipes().values_list('name', flat=True)
624 criteria = Q(name__in=built_recipe_names) & Q(project=self.project)
625 queryset = CustomImageRecipe.objects.filter(criteria).order_by('name')
626 return queryset
627
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500628 def get_outcome_text(self):
629 return Build.BUILD_OUTCOME[int(self.outcome)][1]
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500630
631 @property
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500632 def failed_tasks(self):
633 """ Get failed tasks for the build """
634 tasks = self.task_build.all()
635 return tasks.filter(order__gt=0, outcome=Task.OUTCOME_FAILED)
636
637 @property
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500638 def errors(self):
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500639 return (self.logmessage_set.filter(level=LogMessage.ERROR) |
640 self.logmessage_set.filter(level=LogMessage.EXCEPTION) |
641 self.logmessage_set.filter(level=LogMessage.CRITICAL))
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500642
643 @property
644 def warnings(self):
645 return self.logmessage_set.filter(level=LogMessage.WARNING)
646
647 @property
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500648 def timespent(self):
649 return self.completed_on - self.started_on
650
651 @property
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500652 def timespent_seconds(self):
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500653 return self.timespent.total_seconds()
654
655 @property
656 def target_labels(self):
657 """
658 Sorted (a-z) "target1:task, target2, target3" etc. string for all
659 targets in this build
660 """
661 targets = self.target_set.all()
662 target_labels = [target.target +
663 (':' + target.task if target.task else '')
664 for target in targets]
665 target_labels.sort()
666
667 return target_labels
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500668
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600669 def get_buildrequest(self):
670 buildrequest = None
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500671 if hasattr(self, 'buildrequest'):
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600672 buildrequest = self.buildrequest
673 return buildrequest
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500674
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600675 def is_queued(self):
676 from bldcontrol.models import BuildRequest
677 buildrequest = self.get_buildrequest()
678 if buildrequest:
679 return buildrequest.state == BuildRequest.REQ_QUEUED
680 else:
681 return False
682
683 def is_cancelling(self):
684 from bldcontrol.models import BuildRequest
685 buildrequest = self.get_buildrequest()
686 if buildrequest:
687 return self.outcome == Build.IN_PROGRESS and \
688 buildrequest.state == BuildRequest.REQ_CANCELLING
689 else:
690 return False
691
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500692 def is_cloning(self):
693 """
694 True if the build is still cloning repos
695 """
696 return self.outcome == Build.IN_PROGRESS and \
697 self.repos_cloned < self.repos_to_clone
698
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600699 def is_parsing(self):
700 """
701 True if the build is still parsing recipes
702 """
703 return self.outcome == Build.IN_PROGRESS and \
704 self.recipes_parsed < self.recipes_to_parse
705
706 def is_starting(self):
707 """
708 True if the build has no completed tasks yet and is still just starting
709 tasks.
710
711 Note that the mechanism for testing whether a Task is "done" is whether
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500712 its outcome field is set, as per the completeper() method.
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600713 """
714 return self.outcome == Build.IN_PROGRESS and \
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500715 self.task_build.exclude(outcome=Task.OUTCOME_NA).count() == 0
716
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600717
718 def get_state(self):
719 """
720 Get the state of the build; one of 'Succeeded', 'Failed', 'In Progress',
721 'Cancelled' (Build outcomes); or 'Queued', 'Cancelling' (states
722 dependent on the BuildRequest state).
723
724 This works around the fact that we have BuildRequest states as well
725 as Build states, but really we just want to know the state of the build.
726 """
727 if self.is_cancelling():
728 return 'Cancelling';
729 elif self.is_queued():
730 return 'Queued'
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500731 elif self.is_cloning():
732 return 'Cloning'
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600733 elif self.is_parsing():
734 return 'Parsing'
735 elif self.is_starting():
736 return 'Starting'
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500737 else:
738 return self.get_outcome_text()
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500739
740 def __str__(self):
741 return "%d %s %s" % (self.id, self.project, ",".join([t.target for t in self.target_set.all()]))
742
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500743class ProjectTarget(models.Model):
744 project = models.ForeignKey(Project)
745 target = models.CharField(max_length=100)
746 task = models.CharField(max_length=100, null=True)
747
748class Target(models.Model):
749 search_allowed_fields = ['target', 'file_name']
750 build = models.ForeignKey(Build)
751 target = models.CharField(max_length=100)
752 task = models.CharField(max_length=100, null=True)
753 is_image = models.BooleanField(default = False)
754 image_size = models.IntegerField(default=0)
755 license_manifest_path = models.CharField(max_length=500, null=True)
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600756 package_manifest_path = models.CharField(max_length=500, null=True)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500757
758 def package_count(self):
759 return Target_Installed_Package.objects.filter(target_id__exact=self.id).count()
760
761 def __unicode__(self):
762 return self.target
763
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600764 def get_similar_targets(self):
765 """
766 Get target sfor the same machine, task and target name
767 (e.g. 'core-image-minimal') from a successful build for this project
768 (but excluding this target).
769
770 Note that we only look for targets built by this project because
771 projects can have different configurations from each other, and put
772 their artifacts in different directories.
773
774 The possibility of error when retrieving candidate targets
775 is minimised by the fact that bitbake will rebuild artifacts if MACHINE
776 (or various other variables) change. In this case, there is no need to
777 clone artifacts from another target, as those artifacts will have
778 been re-generated for this target anyway.
779 """
780 query = ~Q(pk=self.pk) & \
781 Q(target=self.target) & \
782 Q(build__machine=self.build.machine) & \
783 Q(build__outcome=Build.SUCCEEDED) & \
784 Q(build__project=self.build.project)
785
786 return Target.objects.filter(query)
787
788 def get_similar_target_with_image_files(self):
789 """
790 Get the most recent similar target with Target_Image_Files associated
791 with it, for the purpose of cloning those files onto this target.
792 """
793 similar_target = None
794
795 candidates = self.get_similar_targets()
796 if candidates.count() == 0:
797 return similar_target
798
799 task_subquery = Q(task=self.task)
800
801 # we can look for a 'build' task if this task is a 'populate_sdk_ext'
802 # task, as the latter also creates images; and vice versa; note that
803 # 'build' targets can have their task set to '';
804 # also note that 'populate_sdk' does not produce image files
805 image_tasks = [
806 '', # aka 'build'
807 'build',
808 'image',
809 'populate_sdk_ext'
810 ]
811 if self.task in image_tasks:
812 task_subquery = Q(task__in=image_tasks)
813
814 # annotate with the count of files, to exclude any targets which
815 # don't have associated files
816 candidates = candidates.annotate(num_files=Count('target_image_file'))
817
818 query = task_subquery & Q(num_files__gt=0)
819
820 candidates = candidates.filter(query)
821
822 if candidates.count() > 0:
823 candidates.order_by('build__completed_on')
824 similar_target = candidates.last()
825
826 return similar_target
827
828 def get_similar_target_with_sdk_files(self):
829 """
830 Get the most recent similar target with TargetSDKFiles associated
831 with it, for the purpose of cloning those files onto this target.
832 """
833 similar_target = None
834
835 candidates = self.get_similar_targets()
836 if candidates.count() == 0:
837 return similar_target
838
839 # annotate with the count of files, to exclude any targets which
840 # don't have associated files
841 candidates = candidates.annotate(num_files=Count('targetsdkfile'))
842
843 query = Q(task=self.task) & Q(num_files__gt=0)
844
845 candidates = candidates.filter(query)
846
847 if candidates.count() > 0:
848 candidates.order_by('build__completed_on')
849 similar_target = candidates.last()
850
851 return similar_target
852
853 def clone_image_artifacts_from(self, target):
854 """
855 Make clones of the Target_Image_Files and TargetKernelFile objects
856 associated with Target target, then associate them with this target.
857
858 Note that for Target_Image_Files, we only want files from the previous
859 build whose suffix matches one of the suffixes defined in this
860 target's build's IMAGE_FSTYPES configuration variable. This prevents the
861 Target_Image_File object for an ext4 image being associated with a
862 target for a project which didn't produce an ext4 image (for example).
863
864 Also sets the license_manifest_path and package_manifest_path
865 of this target to the same path as that of target being cloned from, as
866 the manifests are also build artifacts but are treated differently.
867 """
868
869 image_fstypes = self.build.get_image_fstypes()
870
871 # filter out any image files whose suffixes aren't in the
872 # IMAGE_FSTYPES suffixes variable for this target's build
873 image_files = [target_image_file \
874 for target_image_file in target.target_image_file_set.all() \
875 if target_image_file.suffix in image_fstypes]
876
877 for image_file in image_files:
878 image_file.pk = None
879 image_file.target = self
880 image_file.save()
881
882 kernel_files = target.targetkernelfile_set.all()
883 for kernel_file in kernel_files:
884 kernel_file.pk = None
885 kernel_file.target = self
886 kernel_file.save()
887
888 self.license_manifest_path = target.license_manifest_path
889 self.package_manifest_path = target.package_manifest_path
890 self.save()
891
892 def clone_sdk_artifacts_from(self, target):
893 """
894 Clone TargetSDKFile objects from target and associate them with this
895 target.
896 """
897 sdk_files = target.targetsdkfile_set.all()
898 for sdk_file in sdk_files:
899 sdk_file.pk = None
900 sdk_file.target = self
901 sdk_file.save()
902
903 def has_images(self):
904 """
905 Returns True if this target has one or more image files attached to it.
906 """
907 return self.target_image_file_set.all().count() > 0
908
909# kernel artifacts for a target: bzImage and modules*
910class TargetKernelFile(models.Model):
911 target = models.ForeignKey(Target)
912 file_name = models.FilePathField()
913 file_size = models.IntegerField()
914
915 @property
916 def basename(self):
917 return os.path.basename(self.file_name)
918
919# SDK artifacts for a target: sh and manifest files
920class TargetSDKFile(models.Model):
921 target = models.ForeignKey(Target)
922 file_name = models.FilePathField()
923 file_size = models.IntegerField()
924
925 @property
926 def basename(self):
927 return os.path.basename(self.file_name)
928
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500929class Target_Image_File(models.Model):
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500930 # valid suffixes for image files produced by a build
931 SUFFIXES = {
932 'btrfs', 'cpio', 'cpio.gz', 'cpio.lz4', 'cpio.lzma', 'cpio.xz',
933 'cramfs', 'elf', 'ext2', 'ext2.bz2', 'ext2.gz', 'ext2.lzma', 'ext4',
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600934 'ext4.gz', 'ext3', 'ext3.gz', 'hdddirect', 'hddimg', 'iso', 'jffs2',
935 'jffs2.sum', 'multiubi', 'qcow2', 'squashfs', 'squashfs-lzo',
936 'squashfs-xz', 'tar', 'tar.bz2', 'tar.gz', 'tar.lz4', 'tar.xz', 'ubi',
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500937 'ubifs', 'vdi', 'vmdk', 'wic', 'wic.bmap', 'wic.bz2', 'wic.gz', 'wic.lzma'
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500938 }
939
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500940 target = models.ForeignKey(Target)
941 file_name = models.FilePathField(max_length=254)
942 file_size = models.IntegerField()
943
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500944 @property
945 def suffix(self):
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600946 """
947 Suffix for image file, minus leading "."
948 """
949 for suffix in Target_Image_File.SUFFIXES:
950 if self.file_name.endswith(suffix):
951 return suffix
952
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500953 filename, suffix = os.path.splitext(self.file_name)
954 suffix = suffix.lstrip('.')
955 return suffix
956
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500957class Target_File(models.Model):
958 ITYPE_REGULAR = 1
959 ITYPE_DIRECTORY = 2
960 ITYPE_SYMLINK = 3
961 ITYPE_SOCKET = 4
962 ITYPE_FIFO = 5
963 ITYPE_CHARACTER = 6
964 ITYPE_BLOCK = 7
965 ITYPES = ( (ITYPE_REGULAR ,'regular'),
966 ( ITYPE_DIRECTORY ,'directory'),
967 ( ITYPE_SYMLINK ,'symlink'),
968 ( ITYPE_SOCKET ,'socket'),
969 ( ITYPE_FIFO ,'fifo'),
970 ( ITYPE_CHARACTER ,'character'),
971 ( ITYPE_BLOCK ,'block'),
972 )
973
974 target = models.ForeignKey(Target)
975 path = models.FilePathField()
976 size = models.IntegerField()
977 inodetype = models.IntegerField(choices = ITYPES)
978 permission = models.CharField(max_length=16)
979 owner = models.CharField(max_length=128)
980 group = models.CharField(max_length=128)
981 directory = models.ForeignKey('Target_File', related_name="directory_set", null=True)
982 sym_target = models.ForeignKey('Target_File', related_name="symlink_set", null=True)
983
984
985class Task(models.Model):
986
987 SSTATE_NA = 0
988 SSTATE_MISS = 1
989 SSTATE_FAILED = 2
990 SSTATE_RESTORED = 3
991
992 SSTATE_RESULT = (
993 (SSTATE_NA, 'Not Applicable'), # For rest of tasks, but they still need checking.
994 (SSTATE_MISS, 'File not in cache'), # the sstate object was not found
995 (SSTATE_FAILED, 'Failed'), # there was a pkg, but the script failed
996 (SSTATE_RESTORED, 'Succeeded'), # successfully restored
997 )
998
999 CODING_NA = 0
1000 CODING_PYTHON = 2
1001 CODING_SHELL = 3
1002
1003 TASK_CODING = (
1004 (CODING_NA, 'N/A'),
1005 (CODING_PYTHON, 'Python'),
1006 (CODING_SHELL, 'Shell'),
1007 )
1008
1009 OUTCOME_NA = -1
1010 OUTCOME_SUCCESS = 0
1011 OUTCOME_COVERED = 1
1012 OUTCOME_CACHED = 2
1013 OUTCOME_PREBUILT = 3
1014 OUTCOME_FAILED = 4
1015 OUTCOME_EMPTY = 5
1016
1017 TASK_OUTCOME = (
1018 (OUTCOME_NA, 'Not Available'),
1019 (OUTCOME_SUCCESS, 'Succeeded'),
1020 (OUTCOME_COVERED, 'Covered'),
1021 (OUTCOME_CACHED, 'Cached'),
1022 (OUTCOME_PREBUILT, 'Prebuilt'),
1023 (OUTCOME_FAILED, 'Failed'),
1024 (OUTCOME_EMPTY, 'Empty'),
1025 )
1026
1027 TASK_OUTCOME_HELP = (
1028 (OUTCOME_SUCCESS, 'This task successfully completed'),
1029 (OUTCOME_COVERED, 'This task did not run because its output is provided by another task'),
1030 (OUTCOME_CACHED, 'This task restored output from the sstate-cache directory or mirrors'),
1031 (OUTCOME_PREBUILT, 'This task did not run because its outcome was reused from a previous build'),
1032 (OUTCOME_FAILED, 'This task did not complete'),
1033 (OUTCOME_EMPTY, 'This task has no executable content'),
1034 (OUTCOME_NA, ''),
1035 )
1036
1037 search_allowed_fields = [ "recipe__name", "recipe__version", "task_name", "logfile" ]
1038
1039 def __init__(self, *args, **kwargs):
1040 super(Task, self).__init__(*args, **kwargs)
1041 try:
1042 self._helptext = HelpText.objects.get(key=self.task_name, area=HelpText.VARIABLE, build=self.build).text
1043 except HelpText.DoesNotExist:
1044 self._helptext = None
1045
1046 def get_related_setscene(self):
1047 return Task.objects.filter(task_executed=True, build = self.build, recipe = self.recipe, task_name=self.task_name+"_setscene")
1048
1049 def get_outcome_text(self):
1050 return Task.TASK_OUTCOME[int(self.outcome) + 1][1]
1051
1052 def get_outcome_help(self):
1053 return Task.TASK_OUTCOME_HELP[int(self.outcome)][1]
1054
1055 def get_sstate_text(self):
1056 if self.sstate_result==Task.SSTATE_NA:
1057 return ''
1058 else:
1059 return Task.SSTATE_RESULT[int(self.sstate_result)][1]
1060
1061 def get_executed_display(self):
1062 if self.task_executed:
1063 return "Executed"
1064 return "Not Executed"
1065
1066 def get_description(self):
1067 return self._helptext
1068
1069 build = models.ForeignKey(Build, related_name='task_build')
1070 order = models.IntegerField(null=True)
1071 task_executed = models.BooleanField(default=False) # True means Executed, False means Not/Executed
1072 outcome = models.IntegerField(choices=TASK_OUTCOME, default=OUTCOME_NA)
1073 sstate_checksum = models.CharField(max_length=100, blank=True)
1074 path_to_sstate_obj = models.FilePathField(max_length=500, blank=True)
1075 recipe = models.ForeignKey('Recipe', related_name='tasks')
1076 task_name = models.CharField(max_length=100)
1077 source_url = models.FilePathField(max_length=255, blank=True)
1078 work_directory = models.FilePathField(max_length=255, blank=True)
1079 script_type = models.IntegerField(choices=TASK_CODING, default=CODING_NA)
1080 line_number = models.IntegerField(default=0)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001081
1082 # start/end times
1083 started = models.DateTimeField(null=True)
1084 ended = models.DateTimeField(null=True)
1085
1086 # in seconds; this is stored to enable sorting
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001087 elapsed_time = models.DecimalField(max_digits=8, decimal_places=2, null=True)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001088
1089 # in bytes; note that disk_io is stored to enable sorting
1090 disk_io = models.IntegerField(null=True)
1091 disk_io_read = models.IntegerField(null=True)
1092 disk_io_write = models.IntegerField(null=True)
1093
1094 # in seconds
1095 cpu_time_user = models.DecimalField(max_digits=8, decimal_places=2, null=True)
1096 cpu_time_system = models.DecimalField(max_digits=8, decimal_places=2, null=True)
1097
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001098 sstate_result = models.IntegerField(choices=SSTATE_RESULT, default=SSTATE_NA)
1099 message = models.CharField(max_length=240)
1100 logfile = models.FilePathField(max_length=255, blank=True)
1101
1102 outcome_text = property(get_outcome_text)
1103 sstate_text = property(get_sstate_text)
1104
1105 def __unicode__(self):
1106 return "%d(%d) %s:%s" % (self.pk, self.build.pk, self.recipe.name, self.task_name)
1107
1108 class Meta:
1109 ordering = ('order', 'recipe' ,)
1110 unique_together = ('build', 'recipe', 'task_name', )
1111
1112
1113class Task_Dependency(models.Model):
1114 task = models.ForeignKey(Task, related_name='task_dependencies_task')
1115 depends_on = models.ForeignKey(Task, related_name='task_dependencies_depends')
1116
1117class Package(models.Model):
1118 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']
Patrick Williamsf1e5d692016-03-30 15:21:19 -05001119 build = models.ForeignKey('Build', null=True)
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001120 recipe = models.ForeignKey('Recipe', null=True)
1121 name = models.CharField(max_length=100)
1122 installed_name = models.CharField(max_length=100, default='')
1123 version = models.CharField(max_length=100, blank=True)
1124 revision = models.CharField(max_length=32, blank=True)
1125 summary = models.TextField(blank=True)
1126 description = models.TextField(blank=True)
1127 size = models.IntegerField(default=0)
1128 installed_size = models.IntegerField(default=0)
1129 section = models.CharField(max_length=80, blank=True)
1130 license = models.CharField(max_length=80, blank=True)
1131
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001132 @property
1133 def is_locale_package(self):
1134 """ Returns True if this package is identifiable as a locale package """
1135 if self.name.find('locale') != -1:
1136 return True
1137 return False
1138
1139 @property
1140 def is_packagegroup(self):
1141 """ Returns True is this package is identifiable as a packagegroup """
1142 if self.name.find('packagegroup') != -1:
1143 return True
1144 return False
1145
1146class CustomImagePackage(Package):
1147 # CustomImageRecipe fields to track pacakges appended,
1148 # included and excluded from a CustomImageRecipe
1149 recipe_includes = models.ManyToManyField('CustomImageRecipe',
1150 related_name='includes_set')
1151 recipe_excludes = models.ManyToManyField('CustomImageRecipe',
1152 related_name='excludes_set')
1153 recipe_appends = models.ManyToManyField('CustomImageRecipe',
1154 related_name='appends_set')
1155
1156
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001157class Package_DependencyManager(models.Manager):
1158 use_for_related_fields = True
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001159 TARGET_LATEST = "use-latest-target-for-target"
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001160
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001161 def get_queryset(self):
1162 return super(Package_DependencyManager, self).get_queryset().exclude(package_id = F('depends_on__id'))
1163
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001164 def for_target_or_none(self, target):
1165 """ filter the dependencies to be displayed by the supplied target
1166 if no dependences are found for the target then try None as the target
1167 which will return the dependences calculated without the context of a
1168 target e.g. non image recipes.
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001169
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001170 returns: { size, packages }
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001171 """
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001172 package_dependencies = self.all_depends().order_by('depends_on__name')
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001173
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001174 if target is self.TARGET_LATEST:
1175 installed_deps =\
1176 package_dependencies.filter(~Q(target__target=None))
1177 else:
1178 installed_deps =\
1179 package_dependencies.filter(Q(target__target=target))
1180
1181 packages_list = None
1182 total_size = 0
1183
1184 # If we have installed depdencies for this package and target then use
1185 # these to display
1186 if installed_deps.count() > 0:
1187 packages_list = installed_deps
1188 total_size = installed_deps.aggregate(
1189 Sum('depends_on__size'))['depends_on__size__sum']
1190 else:
1191 new_list = []
1192 package_names = []
1193
1194 # Find dependencies for the package that we know about even if
1195 # it's not installed on a target e.g. from a non-image recipe
1196 for p in package_dependencies.filter(Q(target=None)):
1197 if p.depends_on.name in package_names:
1198 continue
1199 else:
1200 package_names.append(p.depends_on.name)
1201 new_list.append(p.pk)
1202 # while we're here we may as well total up the size to
1203 # avoid iterating again
1204 total_size += p.depends_on.size
1205
1206 # We want to return a queryset here for consistency so pick the
1207 # deps from the new_list
1208 packages_list = package_dependencies.filter(Q(pk__in=new_list))
1209
1210 return {'packages': packages_list,
1211 'size': total_size}
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001212
1213 def all_depends(self):
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001214 """ Returns just the depends packages and not any other dep_type
1215 Note that this is for any target
1216 """
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001217 return self.filter(Q(dep_type=Package_Dependency.TYPE_RDEPENDS) |
1218 Q(dep_type=Package_Dependency.TYPE_TRDEPENDS))
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001219
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001220
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001221class Package_Dependency(models.Model):
1222 TYPE_RDEPENDS = 0
1223 TYPE_TRDEPENDS = 1
1224 TYPE_RRECOMMENDS = 2
1225 TYPE_TRECOMMENDS = 3
1226 TYPE_RSUGGESTS = 4
1227 TYPE_RPROVIDES = 5
1228 TYPE_RREPLACES = 6
1229 TYPE_RCONFLICTS = 7
1230 ' TODO: bpackage should be changed to remove the DEPENDS_TYPE access '
1231 DEPENDS_TYPE = (
1232 (TYPE_RDEPENDS, "depends"),
1233 (TYPE_TRDEPENDS, "depends"),
1234 (TYPE_TRECOMMENDS, "recommends"),
1235 (TYPE_RRECOMMENDS, "recommends"),
1236 (TYPE_RSUGGESTS, "suggests"),
1237 (TYPE_RPROVIDES, "provides"),
1238 (TYPE_RREPLACES, "replaces"),
1239 (TYPE_RCONFLICTS, "conflicts"),
1240 )
1241 """ Indexed by dep_type, in view order, key for short name and help
1242 description which when viewed will be printf'd with the
1243 package name.
1244 """
1245 DEPENDS_DICT = {
1246 TYPE_RDEPENDS : ("depends", "%s is required to run %s"),
1247 TYPE_TRDEPENDS : ("depends", "%s is required to run %s"),
1248 TYPE_TRECOMMENDS : ("recommends", "%s extends the usability of %s"),
1249 TYPE_RRECOMMENDS : ("recommends", "%s extends the usability of %s"),
1250 TYPE_RSUGGESTS : ("suggests", "%s is suggested for installation with %s"),
1251 TYPE_RPROVIDES : ("provides", "%s is provided by %s"),
1252 TYPE_RREPLACES : ("replaces", "%s is replaced by %s"),
1253 TYPE_RCONFLICTS : ("conflicts", "%s conflicts with %s, which will not be installed if this package is not first removed"),
1254 }
1255
1256 package = models.ForeignKey(Package, related_name='package_dependencies_source')
1257 depends_on = models.ForeignKey(Package, related_name='package_dependencies_target') # soft dependency
1258 dep_type = models.IntegerField(choices=DEPENDS_TYPE)
1259 target = models.ForeignKey(Target, null=True)
1260 objects = Package_DependencyManager()
1261
1262class Target_Installed_Package(models.Model):
1263 target = models.ForeignKey(Target)
1264 package = models.ForeignKey(Package, related_name='buildtargetlist_package')
1265
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001266
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001267class Package_File(models.Model):
1268 package = models.ForeignKey(Package, related_name='buildfilelist_package')
1269 path = models.FilePathField(max_length=255, blank=True)
1270 size = models.IntegerField()
1271
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001272
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001273class Recipe(models.Model):
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001274 search_allowed_fields = ['name', 'version', 'file_path', 'section',
1275 'summary', 'description', 'license',
1276 'layer_version__layer__name',
1277 'layer_version__branch', 'layer_version__commit',
1278 'layer_version__local_path',
1279 'layer_version__layer_source']
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001280
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001281 up_date = models.DateTimeField(null=True, default=None)
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001282
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001283 name = models.CharField(max_length=100, blank=True)
1284 version = models.CharField(max_length=100, blank=True)
1285 layer_version = models.ForeignKey('Layer_Version',
1286 related_name='recipe_layer_version')
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001287 summary = models.TextField(blank=True)
1288 description = models.TextField(blank=True)
1289 section = models.CharField(max_length=100, blank=True)
1290 license = models.CharField(max_length=200, blank=True)
1291 homepage = models.URLField(blank=True)
1292 bugtracker = models.URLField(blank=True)
1293 file_path = models.FilePathField(max_length=255)
1294 pathflags = models.CharField(max_length=200, blank=True)
1295 is_image = models.BooleanField(default=False)
1296
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001297 def __unicode__(self):
1298 return "Recipe " + self.name + ":" + self.version
1299
1300 def get_vcs_recipe_file_link_url(self):
1301 return self.layer_version.get_vcs_file_link_url(self.file_path)
1302
1303 def get_description_or_summary(self):
1304 if self.description:
1305 return self.description
1306 elif self.summary:
1307 return self.summary
1308 else:
1309 return ""
1310
1311 class Meta:
1312 unique_together = (("layer_version", "file_path", "pathflags"), )
1313
1314
1315class Recipe_DependencyManager(models.Manager):
1316 use_for_related_fields = True
1317
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001318 def get_queryset(self):
1319 return super(Recipe_DependencyManager, self).get_queryset().exclude(recipe_id = F('depends_on__id'))
1320
1321class Provides(models.Model):
1322 name = models.CharField(max_length=100)
1323 recipe = models.ForeignKey(Recipe)
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001324
1325class Recipe_Dependency(models.Model):
1326 TYPE_DEPENDS = 0
1327 TYPE_RDEPENDS = 1
1328
1329 DEPENDS_TYPE = (
1330 (TYPE_DEPENDS, "depends"),
1331 (TYPE_RDEPENDS, "rdepends"),
1332 )
1333 recipe = models.ForeignKey(Recipe, related_name='r_dependencies_recipe')
1334 depends_on = models.ForeignKey(Recipe, related_name='r_dependencies_depends')
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001335 via = models.ForeignKey(Provides, null=True, default=None)
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001336 dep_type = models.IntegerField(choices=DEPENDS_TYPE)
1337 objects = Recipe_DependencyManager()
1338
1339
1340class Machine(models.Model):
1341 search_allowed_fields = ["name", "description", "layer_version__layer__name"]
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001342 up_date = models.DateTimeField(null = True, default = None)
1343
1344 layer_version = models.ForeignKey('Layer_Version')
1345 name = models.CharField(max_length=255)
1346 description = models.CharField(max_length=255)
1347
1348 def get_vcs_machine_file_link_url(self):
1349 path = 'conf/machine/'+self.name+'.conf'
1350
1351 return self.layer_version.get_vcs_file_link_url(path)
1352
1353 def __unicode__(self):
1354 return "Machine " + self.name + "(" + self.description + ")"
1355
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001356
1357
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001358
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001359
1360class BitbakeVersion(models.Model):
1361
1362 name = models.CharField(max_length=32, unique = True)
1363 giturl = GitURLField()
1364 branch = models.CharField(max_length=32)
1365 dirpath = models.CharField(max_length=255)
1366
1367 def __unicode__(self):
1368 return "%s (Branch: %s)" % (self.name, self.branch)
1369
1370
1371class Release(models.Model):
1372 """ A release is a project template, used to pre-populate Project settings with a configuration set """
1373 name = models.CharField(max_length=32, unique = True)
1374 description = models.CharField(max_length=255)
1375 bitbake_version = models.ForeignKey(BitbakeVersion)
1376 branch_name = models.CharField(max_length=50, default = "")
1377 helptext = models.TextField(null=True)
1378
1379 def __unicode__(self):
1380 return "%s (%s)" % (self.name, self.branch_name)
1381
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001382 def __str__(self):
1383 return self.name
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001384
1385class ReleaseDefaultLayer(models.Model):
1386 release = models.ForeignKey(Release)
1387 layer_name = models.CharField(max_length=100, default="")
1388
1389
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001390class LayerSource(object):
1391 """ Where the layer metadata came from """
1392 TYPE_LOCAL = 0
1393 TYPE_LAYERINDEX = 1
1394 TYPE_IMPORTED = 2
1395 TYPE_BUILD = 3
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001396
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001397 SOURCE_TYPE = (
1398 (TYPE_LOCAL, "local"),
1399 (TYPE_LAYERINDEX, "layerindex"),
1400 (TYPE_IMPORTED, "imported"),
1401 (TYPE_BUILD, "build"),
1402 )
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001403
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001404 def types_dict():
1405 """ Turn the TYPES enums into a simple dictionary """
1406 dictionary = {}
1407 for key in LayerSource.__dict__:
1408 if "TYPE" in key:
1409 dictionary[key] = getattr(LayerSource, key)
1410 return dictionary
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001411
1412
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001413class Layer(models.Model):
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001414
1415 up_date = models.DateTimeField(null=True, default=timezone.now)
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001416
1417 name = models.CharField(max_length=100)
1418 layer_index_url = models.URLField()
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001419 vcs_url = GitURLField(default=None, null=True)
Brad Bishop6e60e8b2018-02-01 10:27:11 -05001420 local_source_dir = models.TextField(null=True, default=None)
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001421 vcs_web_url = models.URLField(null=True, default=None)
1422 vcs_web_tree_base_url = models.URLField(null=True, default=None)
1423 vcs_web_file_base_url = models.URLField(null=True, default=None)
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001424
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001425 summary = models.TextField(help_text='One-line description of the layer',
1426 null=True, default=None)
1427 description = models.TextField(null=True, default=None)
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001428
1429 def __unicode__(self):
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001430 return "%s / %s " % (self.name, self.summary)
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001431
1432
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001433class Layer_Version(models.Model):
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001434 """
1435 A Layer_Version either belongs to a single project or no project
1436 """
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001437 search_allowed_fields = ["layer__name", "layer__summary",
1438 "layer__description", "layer__vcs_url",
1439 "dirpath", "release__name", "commit", "branch"]
1440
1441 build = models.ForeignKey(Build, related_name='layer_version_build',
1442 default=None, null=True)
1443
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001444 layer = models.ForeignKey(Layer, related_name='layer_version_layer')
1445
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001446 layer_source = models.IntegerField(choices=LayerSource.SOURCE_TYPE,
1447 default=0)
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001448
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001449 up_date = models.DateTimeField(null=True, default=timezone.now)
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001450
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001451 # To which metadata release does this layer version belong to
1452 release = models.ForeignKey(Release, null=True, default=None)
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001453
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001454 branch = models.CharField(max_length=80)
1455 commit = models.CharField(max_length=100)
1456 # If the layer is in a subdir
1457 dirpath = models.CharField(max_length=255, null=True, default=None)
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001458
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001459 # if -1, this is a default layer
1460 priority = models.IntegerField(default=0)
1461
1462 # where this layer exists on the filesystem
1463 local_path = models.FilePathField(max_length=1024, default="/")
1464
1465 # Set if this layer is restricted to a particular project
1466 project = models.ForeignKey('Project', null=True, default=None)
1467
1468 # code lifted, with adaptations, from the layerindex-web application
1469 # https://git.yoctoproject.org/cgit/cgit.cgi/layerindex-web/
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001470 def _handle_url_path(self, base_url, path):
1471 import re, posixpath
1472 if base_url:
1473 if self.dirpath:
1474 if path:
1475 extra_path = self.dirpath + '/' + path
1476 # Normalise out ../ in path for usage URL
1477 extra_path = posixpath.normpath(extra_path)
1478 # Minor workaround to handle case where subdirectory has been added between branches
1479 # (should probably support usage URL per branch to handle this... sigh...)
1480 if extra_path.startswith('../'):
1481 extra_path = extra_path[3:]
1482 else:
1483 extra_path = self.dirpath
1484 else:
1485 extra_path = path
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001486 branchname = self.release.name
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001487 url = base_url.replace('%branch%', branchname)
1488
1489 # If there's a % in the path (e.g. a wildcard bbappend) we need to encode it
1490 if extra_path:
1491 extra_path = extra_path.replace('%', '%25')
1492
1493 if '%path%' in base_url:
1494 if extra_path:
1495 url = re.sub(r'\[([^\]]*%path%[^\]]*)\]', '\\1', url)
1496 else:
1497 url = re.sub(r'\[([^\]]*%path%[^\]]*)\]', '', url)
1498 return url.replace('%path%', extra_path)
1499 else:
1500 return url + extra_path
1501 return None
1502
1503 def get_vcs_link_url(self):
1504 if self.layer.vcs_web_url is None:
1505 return None
1506 return self.layer.vcs_web_url
1507
1508 def get_vcs_file_link_url(self, file_path=""):
1509 if self.layer.vcs_web_file_base_url is None:
1510 return None
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001511 return self._handle_url_path(self.layer.vcs_web_file_base_url,
1512 file_path)
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001513
1514 def get_vcs_dirpath_link_url(self):
1515 if self.layer.vcs_web_tree_base_url is None:
1516 return None
1517 return self._handle_url_path(self.layer.vcs_web_tree_base_url, '')
1518
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001519 def get_vcs_reference(self):
Brad Bishopd7bf8c12018-02-25 22:55:05 -05001520 if self.commit is not None and len(self.commit) > 0:
1521 return self.commit
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001522 if self.branch is not None and len(self.branch) > 0:
1523 return self.branch
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001524 if self.release is not None:
1525 return self.release.name
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001526 return 'N/A'
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001527
Brad Bishop6e60e8b2018-02-01 10:27:11 -05001528 def get_detailspage_url(self, project_id=None):
1529 """ returns the url to the layer details page uses own project
1530 field if project_id is not specified """
1531
1532 if project_id is None:
1533 project_id = self.project.pk
1534
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001535 return reverse('layerdetails', args=(project_id, self.pk))
1536
Patrick Williamsf1e5d692016-03-30 15:21:19 -05001537 def get_alldeps(self, project_id):
1538 """Get full list of unique layer dependencies."""
Brad Bishop6e60e8b2018-02-01 10:27:11 -05001539 def gen_layerdeps(lver, project, depth):
1540 if depth == 0:
1541 return
Patrick Williamsf1e5d692016-03-30 15:21:19 -05001542 for ldep in lver.dependencies.all():
1543 yield ldep.depends_on
1544 # get next level of deps recursively calling gen_layerdeps
Brad Bishop6e60e8b2018-02-01 10:27:11 -05001545 for subdep in gen_layerdeps(ldep.depends_on, project, depth-1):
Patrick Williamsf1e5d692016-03-30 15:21:19 -05001546 yield subdep
1547
1548 project = Project.objects.get(pk=project_id)
1549 result = []
Brad Bishop6e60e8b2018-02-01 10:27:11 -05001550 projectlvers = [player.layercommit for player in
1551 project.projectlayer_set.all()]
1552 # protect against infinite layer dependency loops
1553 maxdepth = 20
1554 for dep in gen_layerdeps(self, project, maxdepth):
Patrick Williamsf1e5d692016-03-30 15:21:19 -05001555 # filter out duplicates and layers already belonging to the project
1556 if dep not in result + projectlvers:
1557 result.append(dep)
1558
1559 return sorted(result, key=lambda x: x.layer.name)
1560
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001561 def __unicode__(self):
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001562 return ("id %d belongs to layer: %s" % (self.pk, self.layer.name))
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001563
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001564 def __str__(self):
1565 if self.release:
1566 release = self.release.name
1567 else:
1568 release = "No release set"
1569
1570 return "%d %s (%s)" % (self.pk, self.layer.name, release)
1571
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001572
1573class LayerVersionDependency(models.Model):
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001574
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001575 layer_version = models.ForeignKey(Layer_Version,
1576 related_name="dependencies")
1577 depends_on = models.ForeignKey(Layer_Version,
1578 related_name="dependees")
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001579
1580class ProjectLayer(models.Model):
1581 project = models.ForeignKey(Project)
1582 layercommit = models.ForeignKey(Layer_Version, null=True)
1583 optional = models.BooleanField(default = True)
1584
1585 def __unicode__(self):
1586 return "%s, %s" % (self.project.name, self.layercommit)
1587
1588 class Meta:
1589 unique_together = (("project", "layercommit"),)
1590
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001591class CustomImageRecipe(Recipe):
Patrick Williamsf1e5d692016-03-30 15:21:19 -05001592
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001593 # CustomImageRecipe's belong to layers called:
1594 LAYER_NAME = "toaster-custom-images"
1595
1596 search_allowed_fields = ['name']
1597 base_recipe = models.ForeignKey(Recipe, related_name='based_on_recipe')
1598 project = models.ForeignKey(Project)
1599 last_updated = models.DateTimeField(null=True, default=None)
1600
1601 def get_last_successful_built_target(self):
1602 """ Return the last successful built target object if one exists
1603 otherwise return None """
1604 return Target.objects.filter(Q(build__outcome=Build.SUCCEEDED) &
1605 Q(build__project=self.project) &
1606 Q(target=self.name)).last()
1607
1608 def update_package_list(self):
1609 """ Update the package list from the last good build of this
1610 CustomImageRecipe
1611 """
1612 # Check if we're aldready up-to-date or not
1613 target = self.get_last_successful_built_target()
1614 if target == None:
1615 # So we've never actually built this Custom recipe but what about
1616 # the recipe it's based on?
1617 target = \
1618 Target.objects.filter(Q(build__outcome=Build.SUCCEEDED) &
1619 Q(build__project=self.project) &
1620 Q(target=self.base_recipe.name)).last()
1621 if target == None:
1622 return
1623
1624 if target.build.completed_on == self.last_updated:
1625 return
1626
1627 self.includes_set.clear()
1628
1629 excludes_list = self.excludes_set.values_list('name', flat=True)
1630 appends_list = self.appends_set.values_list('name', flat=True)
1631
1632 built_packages_list = \
1633 target.target_installed_package_set.values_list('package__name',
1634 flat=True)
1635 for built_package in built_packages_list:
1636 # Is the built package in the custom packages list?
1637 if built_package in excludes_list:
1638 continue
1639
1640 if built_package in appends_list:
1641 continue
1642
1643 cust_img_p = \
1644 CustomImagePackage.objects.get(name=built_package)
1645 self.includes_set.add(cust_img_p)
1646
1647
1648 self.last_updated = target.build.completed_on
1649 self.save()
1650
1651 def get_all_packages(self):
1652 """Get the included packages and any appended packages"""
1653 self.update_package_list()
1654
1655 return CustomImagePackage.objects.filter((Q(recipe_appends=self) |
1656 Q(recipe_includes=self)) &
1657 ~Q(recipe_excludes=self))
1658
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001659 def get_base_recipe_file(self):
1660 """Get the base recipe file path if it exists on the file system"""
Brad Bishopd7bf8c12018-02-25 22:55:05 -05001661 path_schema_one = "%s/%s" % (self.base_recipe.layer_version.local_path,
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001662 self.base_recipe.file_path)
1663
1664 path_schema_two = self.base_recipe.file_path
1665
Brad Bishop5dd7cbb2018-09-05 22:26:40 -07001666 path_schema_three = "%s/%s" % (self.base_recipe.layer_version.layer.local_source_dir,
1667 self.base_recipe.file_path)
1668
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001669 if os.path.exists(path_schema_one):
1670 return path_schema_one
1671
1672 # The path may now be the full path if the recipe has been built
1673 if os.path.exists(path_schema_two):
1674 return path_schema_two
1675
Brad Bishop5dd7cbb2018-09-05 22:26:40 -07001676 # Or a local path if all layers are local
1677 if os.path.exists(path_schema_three):
1678 return path_schema_three
1679
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001680 return None
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001681
1682 def generate_recipe_file_contents(self):
1683 """Generate the contents for the recipe file."""
1684 # If we have no excluded packages we only need to _append
1685 if self.excludes_set.count() == 0:
1686 packages_conf = "IMAGE_INSTALL_append = \" "
1687
1688 for pkg in self.appends_set.all():
1689 packages_conf += pkg.name+' '
1690 else:
1691 packages_conf = "IMAGE_FEATURES =\"\"\nIMAGE_INSTALL = \""
1692 # We add all the known packages to be built by this recipe apart
1693 # from locale packages which are are controlled with IMAGE_LINGUAS.
1694 for pkg in self.get_all_packages().exclude(
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001695 name__icontains="locale"):
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001696 packages_conf += pkg.name+' '
1697
1698 packages_conf += "\""
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001699
1700 base_recipe_path = self.get_base_recipe_file()
1701 if base_recipe_path:
1702 base_recipe = open(base_recipe_path, 'r').read()
1703 else:
Brad Bishop6e60e8b2018-02-01 10:27:11 -05001704 raise IOError("Based on recipe file not found: %s" %
1705 base_recipe_path)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001706
1707 # Add a special case for when the recipe we have based a custom image
1708 # recipe on requires another recipe.
1709 # For example:
1710 # "require core-image-minimal.bb" is changed to:
1711 # "require recipes-core/images/core-image-minimal.bb"
1712
1713 req_search = re.search(r'(require\s+)(.+\.bb\s*$)',
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001714 base_recipe,
1715 re.MULTILINE)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001716 if req_search:
1717 require_filename = req_search.group(2).strip()
1718
1719 corrected_location = Recipe.objects.filter(
1720 Q(layer_version=self.base_recipe.layer_version) &
1721 Q(file_path__icontains=require_filename)).last().file_path
1722
1723 new_require_line = "require %s" % corrected_location
1724
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001725 base_recipe = base_recipe.replace(req_search.group(0),
1726 new_require_line)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001727
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001728 info = {
1729 "date": timezone.now().strftime("%Y-%m-%d %H:%M:%S"),
1730 "base_recipe": base_recipe,
1731 "recipe_name": self.name,
1732 "base_recipe_name": self.base_recipe.name,
1733 "license": self.license,
1734 "summary": self.summary,
1735 "description": self.description,
1736 "packages_conf": packages_conf.strip()
1737 }
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001738
1739 recipe_contents = ("# Original recipe %(base_recipe_name)s \n"
1740 "%(base_recipe)s\n\n"
1741 "# Recipe %(recipe_name)s \n"
1742 "# Customisation Generated by Toaster on %(date)s\n"
1743 "SUMMARY = \"%(summary)s\"\n"
1744 "DESCRIPTION = \"%(description)s\"\n"
1745 "LICENSE = \"%(license)s\"\n"
1746 "%(packages_conf)s") % info
1747
1748 return recipe_contents
Patrick Williamsf1e5d692016-03-30 15:21:19 -05001749
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001750class ProjectVariable(models.Model):
1751 project = models.ForeignKey(Project)
1752 name = models.CharField(max_length=100)
1753 value = models.TextField(blank = True)
1754
1755class Variable(models.Model):
1756 search_allowed_fields = ['variable_name', 'variable_value',
1757 'vhistory__file_name', "description"]
1758 build = models.ForeignKey(Build, related_name='variable_build')
1759 variable_name = models.CharField(max_length=100)
1760 variable_value = models.TextField(blank=True)
1761 changed = models.BooleanField(default=False)
1762 human_readable_name = models.CharField(max_length=200)
1763 description = models.TextField(blank=True)
1764
1765class VariableHistory(models.Model):
1766 variable = models.ForeignKey(Variable, related_name='vhistory')
1767 value = models.TextField(blank=True)
1768 file_name = models.FilePathField(max_length=255)
1769 line_number = models.IntegerField(null=True)
1770 operation = models.CharField(max_length=64)
1771
1772class HelpText(models.Model):
1773 VARIABLE = 0
1774 HELPTEXT_AREA = ((VARIABLE, 'variable'), )
1775
1776 build = models.ForeignKey(Build, related_name='helptext_build')
1777 area = models.IntegerField(choices=HELPTEXT_AREA)
1778 key = models.CharField(max_length=100)
1779 text = models.TextField()
1780
1781class LogMessage(models.Model):
1782 EXCEPTION = -1 # used to signal self-toaster-exceptions
1783 INFO = 0
1784 WARNING = 1
1785 ERROR = 2
Patrick Williamsf1e5d692016-03-30 15:21:19 -05001786 CRITICAL = 3
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001787
Patrick Williamsf1e5d692016-03-30 15:21:19 -05001788 LOG_LEVEL = (
1789 (INFO, "info"),
1790 (WARNING, "warn"),
1791 (ERROR, "error"),
1792 (CRITICAL, "critical"),
1793 (EXCEPTION, "toaster exception")
1794 )
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001795
1796 build = models.ForeignKey(Build)
1797 task = models.ForeignKey(Task, blank = True, null=True)
1798 level = models.IntegerField(choices=LOG_LEVEL, default=INFO)
Patrick Williamsf1e5d692016-03-30 15:21:19 -05001799 message = models.TextField(blank=True, null=True)
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001800 pathname = models.FilePathField(max_length=255, blank=True)
1801 lineno = models.IntegerField(null=True)
1802
1803 def __str__(self):
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001804 return force_bytes('%s %s %s' % (self.get_level_display(), self.message, self.build))
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001805
1806def invalidate_cache(**kwargs):
1807 from django.core.cache import cache
1808 try:
1809 cache.clear()
1810 except Exception as e:
1811 logger.warning("Problem with cache backend: Failed to clear cache: %s" % e)
1812
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001813def signal_runbuilds():
1814 """Send SIGUSR1 to runbuilds process"""
Brad Bishop6e60e8b2018-02-01 10:27:11 -05001815 try:
1816 with open(os.path.join(os.getenv('BUILDDIR', '.'),
1817 '.runbuilds.pid')) as pidf:
1818 os.kill(int(pidf.read()), SIGUSR1)
1819 except FileNotFoundError:
1820 logger.info("Stopping existing runbuilds: no current process found")
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001821
Brad Bishopd7bf8c12018-02-25 22:55:05 -05001822class Distro(models.Model):
1823 search_allowed_fields = ["name", "description", "layer_version__layer__name"]
1824 up_date = models.DateTimeField(null = True, default = None)
1825
1826 layer_version = models.ForeignKey('Layer_Version')
1827 name = models.CharField(max_length=255)
1828 description = models.CharField(max_length=255)
1829
1830 def get_vcs_distro_file_link_url(self):
1831 path = self.name+'.conf'
1832 return self.layer_version.get_vcs_file_link_url(path)
1833
1834 def __unicode__(self):
1835 return "Distro " + self.name + "(" + self.description + ")"
1836
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001837django.db.models.signals.post_save.connect(invalidate_cache)
1838django.db.models.signals.post_delete.connect(invalidate_cache)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001839django.db.models.signals.m2m_changed.connect(invalidate_cache)