blob: 7720290214e282b949d5d79738753092fd32d858 [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):
Brad Bishop1a4b7ee2018-12-16 17:11:34 -0800124 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:
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600133 prj = self.model(name=name,
134 bitbake_version=release.bitbake_version,
135 release=release)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500136 else:
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600137 prj = self.model(name=name,
138 bitbake_version=None,
139 release=None)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500140 prj.save()
141
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600142 for defaultconf in ToasterSetting.objects.filter(
143 name__startswith="DEFCONF_"):
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500144 name = defaultconf.name[8:]
Brad Bishop1a4b7ee2018-12-16 17:11:34 -0800145 pv,create = ProjectVariable.objects.get_or_create(project=prj,name=name)
146 pv.value = defaultconf.value
147 pv.save()
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500148
149 if release is None:
150 return prj
151
152 for rdl in release.releasedefaultlayer_set.all():
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600153 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)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500164
165 return prj
166
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500167 # return single object with is_default = True
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500168 def get_or_create_default_project(self):
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600169 projects = super(ProjectManager, self).filter(is_default=True)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500170
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500171 if len(projects) > 1:
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500172 raise Exception('Inconsistent project data: multiple ' +
173 'default projects (i.e. with is_default=True)')
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500174 elif len(projects) < 1:
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500175 options = {
176 'name': 'Command line builds',
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600177 'short_description':
178 'Project for builds started outside Toaster',
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500179 'is_default': True
180 }
181 project = Project.objects.create(**options)
182 project.save()
183
184 return project
185 else:
186 return projects[0]
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500187
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500188
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500189class Project(models.Model):
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500190 search_allowed_fields = ['name', 'short_description', 'release__name',
191 'release__branch_name']
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500192 name = models.CharField(max_length=100)
193 short_description = models.CharField(max_length=50, blank=True)
194 bitbake_version = models.ForeignKey('BitbakeVersion', null=True)
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500195 release = models.ForeignKey("Release", null=True)
196 created = models.DateTimeField(auto_now_add=True)
197 updated = models.DateTimeField(auto_now=True)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500198 # 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
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500200 # Instead, we keep a possible null reference to the User id,
201 # as not to force
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500202 # hard links to possibly missing models
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500203 user_id = models.IntegerField(null=True)
204 objects = ProjectManager()
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500205
Brad Bishop1a4b7ee2018-12-16 17:11:34 -0800206 # 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
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500211 # set to True for the project which is the default container
212 # for builds initiated by the command line etc.
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500213 is_default= models.BooleanField(default=False)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500214
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):
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500222 return None;
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500223
224 def get_number_of_builds(self):
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500225 """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()
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500231
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):
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500239 build_id = self.get_last_build_id()
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500240 if (-1 == build_id):
241 return( "" )
242 try:
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500243 return Build.objects.filter( id = build_id )[ 0 ].outcome
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500244 except (Build.DoesNotExist,IndexError):
245 return( "not_found" )
246
247 def get_last_target(self):
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500248 build_id = self.get_last_build_id()
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500249 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):
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500257 build_id = self.get_last_build_id()
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500258 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):
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500266 build_id = self.get_last_build_id()
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500267 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
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500274 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
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500282 def get_last_imgfiles(self):
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500283 build_id = self.get_last_build_id()
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500284 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
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500291 def get_all_compatible_layer_versions(self):
292 """ Returns Queryset of all Layer_Versions which are compatible with
293 this project"""
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500294 queryset = None
295
296 # guard on release, as it can be null
297 if self.release:
298 queryset = Layer_Version.objects.filter(
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600299 (Q(release=self.release) &
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500300 Q(build=None) &
301 Q(project=None)) |
302 Q(project=self))
303 else:
304 queryset = Layer_Version.objects.none()
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500305
306 return queryset
307
308 def get_project_layer_versions(self, pk=False):
309 """ Returns the Layer_Versions currently added to this project """
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500310 layer_versions = self.projectlayer_set.all().values_list('layercommit',
311 flat=True)
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500312
313 if pk is False:
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500314 return Layer_Version.objects.filter(pk__in=layer_versions)
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500315 else:
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500316 return layer_versions
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500317
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500318
Brad Bishop1a4b7ee2018-12-16 17:11:34 -0800319 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
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500328 def get_available_machines(self):
329 """ Returns QuerySet of all Machines which are provided by the
330 Layers currently added to the Project """
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500331 queryset = Machine.objects.filter(
332 layer_version__in=self.get_project_layer_versions())
333
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500334 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 """
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500339 queryset = Machine.objects.filter(
340 layer_version__in=self.get_all_compatible_layer_versions())
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500341
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500342 return queryset
343
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500344 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
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500360 def get_available_recipes(self):
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500361 """ 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())
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500365
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 """
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500371 queryset = Recipe.objects.filter(
372 layer_version__in=self.get_all_compatible_layer_versions()).exclude(name__exact='')
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500373
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500374 return queryset
375
Brad Bishop1a4b7ee2018-12-16 17:11:34 -0800376 # 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
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500402 def schedule_build(self):
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500403
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500404 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)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500436
437 for l in self.projectlayer_set.all().order_by("pk"):
438 commit = l.layercommit.get_vcs_reference()
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500439 logger.debug("Adding layer to build %s" %
440 l.layercommit.layer.name)
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600441 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 )
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500450
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500451 for v in self.projectvariable_set.all():
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500452 BRVariable.objects.create(req=br, name=v.name, value=v.value)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500453
454 try:
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500455 br.build.machine = self.projectvariable_set.get(
456 name='MACHINE').value
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500457 br.build.save()
458 except ProjectVariable.DoesNotExist:
459 pass
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500460
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500461 br.save()
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600462 signal_runbuilds()
463
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500464 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
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500474 CANCELLED = 3
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500475
476 BUILD_OUTCOME = (
477 (SUCCEEDED, 'Succeeded'),
478 (FAILED, 'Failed'),
479 (IN_PROGRESS, 'In Progress'),
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500480 (CANCELLED, 'Cancelled'),
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500481 )
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)
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600493 build_name = models.CharField(max_length=100, default='')
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500494 bitbake_version = models.CharField(max_length=50)
495
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600496 # 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
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500500 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)
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600507
Brad Bishop1a4b7ee2018-12-16 17:11:34 -0800508 # Hint on current progress item
509 progress_item = models.CharField(max_length=40)
510
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500511 @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()
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600537 build.outcomeText = build.get_outcome_text()
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500538
539 return recent_builds
540
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600541 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
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500556 def completeper(self):
557 tf = Task.objects.filter(build = self)
558 tfc = tf.count()
559 if tfc > 0:
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500560 completeper = tf.exclude(outcome=Task.OUTCOME_NA).count()*100 // tfc
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500561 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
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600572 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
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500593 def get_image_file_extensions(self):
594 """
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600595 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"
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500600 """
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500601 extensions = []
602
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600603 targets = Target.objects.filter(build_id = self.id)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500604 for target in targets:
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600605 if not target.is_image:
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500606 continue
607
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600608 target_image_files = Target_Image_File.objects.filter(
609 target_id=target.id)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500610
611 for target_image_file in target_image_files:
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600612 extensions.append(target_image_file.suffix)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500613
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600614 extensions = list(set(extensions))
615 extensions.sort()
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500616
617 return ', '.join(extensions)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500618
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600619 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
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500628 def get_sorted_target_list(self):
629 tgts = Target.objects.filter(build_id = self.id).order_by( 'target' );
630 return( tgts );
631
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500632 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
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500677 def get_outcome_text(self):
678 return Build.BUILD_OUTCOME[int(self.outcome)][1]
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500679
680 @property
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500681 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
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500687 def errors(self):
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500688 return (self.logmessage_set.filter(level=LogMessage.ERROR) |
689 self.logmessage_set.filter(level=LogMessage.EXCEPTION) |
690 self.logmessage_set.filter(level=LogMessage.CRITICAL))
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500691
692 @property
693 def warnings(self):
694 return self.logmessage_set.filter(level=LogMessage.WARNING)
695
696 @property
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500697 def timespent(self):
698 return self.completed_on - self.started_on
699
700 @property
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500701 def timespent_seconds(self):
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500702 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
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500717
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600718 def get_buildrequest(self):
719 buildrequest = None
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500720 if hasattr(self, 'buildrequest'):
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600721 buildrequest = self.buildrequest
722 return buildrequest
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500723
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600724 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
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500741 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
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600748 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
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500761 its outcome field is set, as per the completeper() method.
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600762 """
763 return self.outcome == Build.IN_PROGRESS and \
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500764 self.task_build.exclude(outcome=Task.OUTCOME_NA).count() == 0
765
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600766
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'
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500780 elif self.is_cloning():
781 return 'Cloning'
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600782 elif self.is_parsing():
783 return 'Parsing'
784 elif self.is_starting():
785 return 'Starting'
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500786 else:
787 return self.get_outcome_text()
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500788
789 def __str__(self):
790 return "%d %s %s" % (self.id, self.project, ",".join([t.target for t in self.target_set.all()]))
791
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500792class 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)
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600805 package_manifest_path = models.CharField(max_length=500, null=True)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500806
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
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600813 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
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500978class Target_Image_File(models.Model):
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500979 # 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',
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600983 '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',
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500986 'ubifs', 'vdi', 'vmdk', 'wic', 'wic.bmap', 'wic.bz2', 'wic.gz', 'wic.lzma'
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500987 }
988
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500989 target = models.ForeignKey(Target)
990 file_name = models.FilePathField(max_length=254)
991 file_size = models.IntegerField()
992
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500993 @property
994 def suffix(self):
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600995 """
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
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001002 filename, suffix = os.path.splitext(self.file_name)
1003 suffix = suffix.lstrip('.')
1004 return suffix
1005
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001006class 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)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001130
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
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001136 elapsed_time = models.DecimalField(max_digits=8, decimal_places=2, null=True)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001137
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
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001147 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']
Patrick Williamsf1e5d692016-03-30 15:21:19 -05001168 build = models.ForeignKey('Build', null=True)
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001169 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
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001181 @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
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001206class Package_DependencyManager(models.Manager):
1207 use_for_related_fields = True
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001208 TARGET_LATEST = "use-latest-target-for-target"
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001209
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001210 def get_queryset(self):
1211 return super(Package_DependencyManager, self).get_queryset().exclude(package_id = F('depends_on__id'))
1212
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001213 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.
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001218
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001219 returns: { size, packages }
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001220 """
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001221 package_dependencies = self.all_depends().order_by('depends_on__name')
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001222
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001223 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}
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001261
1262 def all_depends(self):
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001263 """ Returns just the depends packages and not any other dep_type
1264 Note that this is for any target
1265 """
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001266 return self.filter(Q(dep_type=Package_Dependency.TYPE_RDEPENDS) |
1267 Q(dep_type=Package_Dependency.TYPE_TRDEPENDS))
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001268
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001269
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001270class 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
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001315
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001316class 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
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001321
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001322class Recipe(models.Model):
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001323 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']
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001329
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001330 up_date = models.DateTimeField(null=True, default=None)
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001331
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001332 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')
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001336 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
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001346 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
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001367 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)
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001373
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')
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001384 via = models.ForeignKey(Provides, null=True, default=None)
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001385 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"]
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001391 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
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001405
1406
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001407
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001408
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
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001431 def __str__(self):
1432 return self.name
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001433
1434class ReleaseDefaultLayer(models.Model):
1435 release = models.ForeignKey(Release)
1436 layer_name = models.CharField(max_length=100, default="")
1437
1438
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001439class 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
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001445
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001446 SOURCE_TYPE = (
1447 (TYPE_LOCAL, "local"),
1448 (TYPE_LAYERINDEX, "layerindex"),
1449 (TYPE_IMPORTED, "imported"),
1450 (TYPE_BUILD, "build"),
1451 )
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001452
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001453 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
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001460
1461
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001462class Layer(models.Model):
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001463
1464 up_date = models.DateTimeField(null=True, default=timezone.now)
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001465
1466 name = models.CharField(max_length=100)
1467 layer_index_url = models.URLField()
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001468 vcs_url = GitURLField(default=None, null=True)
Brad Bishop6e60e8b2018-02-01 10:27:11 -05001469 local_source_dir = models.TextField(null=True, default=None)
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001470 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)
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001473
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001474 summary = models.TextField(help_text='One-line description of the layer',
1475 null=True, default=None)
1476 description = models.TextField(null=True, default=None)
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001477
1478 def __unicode__(self):
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001479 return "%s / %s " % (self.name, self.summary)
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001480
1481
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001482class Layer_Version(models.Model):
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001483 """
1484 A Layer_Version either belongs to a single project or no project
1485 """
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001486 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
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001493 layer = models.ForeignKey(Layer, related_name='layer_version_layer')
1494
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001495 layer_source = models.IntegerField(choices=LayerSource.SOURCE_TYPE,
1496 default=0)
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001497
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001498 up_date = models.DateTimeField(null=True, default=timezone.now)
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001499
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001500 # To which metadata release does this layer version belong to
1501 release = models.ForeignKey(Release, null=True, default=None)
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001502
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001503 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)
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001507
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001508 # 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/
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001519 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
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001535 branchname = self.release.name
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001536 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
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001560 return self._handle_url_path(self.layer.vcs_web_file_base_url,
1561 file_path)
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001562
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
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001568 def get_vcs_reference(self):
Brad Bishopd7bf8c12018-02-25 22:55:05 -05001569 if self.commit is not None and len(self.commit) > 0:
1570 return self.commit
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001571 if self.branch is not None and len(self.branch) > 0:
1572 return self.branch
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001573 if self.release is not None:
1574 return self.release.name
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001575 return 'N/A'
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001576
Brad Bishop6e60e8b2018-02-01 10:27:11 -05001577 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
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001584 return reverse('layerdetails', args=(project_id, self.pk))
1585
Patrick Williamsf1e5d692016-03-30 15:21:19 -05001586 def get_alldeps(self, project_id):
1587 """Get full list of unique layer dependencies."""
Brad Bishop6e60e8b2018-02-01 10:27:11 -05001588 def gen_layerdeps(lver, project, depth):
1589 if depth == 0:
1590 return
Patrick Williamsf1e5d692016-03-30 15:21:19 -05001591 for ldep in lver.dependencies.all():
1592 yield ldep.depends_on
1593 # get next level of deps recursively calling gen_layerdeps
Brad Bishop6e60e8b2018-02-01 10:27:11 -05001594 for subdep in gen_layerdeps(ldep.depends_on, project, depth-1):
Patrick Williamsf1e5d692016-03-30 15:21:19 -05001595 yield subdep
1596
1597 project = Project.objects.get(pk=project_id)
1598 result = []
Brad Bishop6e60e8b2018-02-01 10:27:11 -05001599 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):
Patrick Williamsf1e5d692016-03-30 15:21:19 -05001604 # 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
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001610 def __unicode__(self):
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001611 return ("id %d belongs to layer: %s" % (self.pk, self.layer.name))
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001612
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001613 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
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001621
1622class LayerVersionDependency(models.Model):
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001623
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001624 layer_version = models.ForeignKey(Layer_Version,
1625 related_name="dependencies")
1626 depends_on = models.ForeignKey(Layer_Version,
1627 related_name="dependees")
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001628
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
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001640class CustomImageRecipe(Recipe):
Patrick Williamsf1e5d692016-03-30 15:21:19 -05001641
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001642 # 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
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001708 def get_base_recipe_file(self):
1709 """Get the base recipe file path if it exists on the file system"""
Brad Bishopd7bf8c12018-02-25 22:55:05 -05001710 path_schema_one = "%s/%s" % (self.base_recipe.layer_version.local_path,
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001711 self.base_recipe.file_path)
1712
1713 path_schema_two = self.base_recipe.file_path
1714
Brad Bishop5dd7cbb2018-09-05 22:26:40 -07001715 path_schema_three = "%s/%s" % (self.base_recipe.layer_version.layer.local_source_dir,
1716 self.base_recipe.file_path)
1717
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001718 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
Brad Bishop5dd7cbb2018-09-05 22:26:40 -07001725 # Or a local path if all layers are local
1726 if os.path.exists(path_schema_three):
1727 return path_schema_three
1728
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001729 return None
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001730
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(
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001744 name__icontains="locale"):
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001745 packages_conf += pkg.name+' '
1746
1747 packages_conf += "\""
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001748
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:
Brad Bishop1a4b7ee2018-12-16 17:11:34 -08001753 # Pass back None to trigger error message to user
1754 return None
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001755
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*$)',
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001763 base_recipe,
1764 re.MULTILINE)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001765 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
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001774 base_recipe = base_recipe.replace(req_search.group(0),
1775 new_require_line)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001776
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001777 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 }
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001787
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
Patrick Williamsf1e5d692016-03-30 15:21:19 -05001798
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001799class 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
Patrick Williamsf1e5d692016-03-30 15:21:19 -05001835 CRITICAL = 3
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001836
Patrick Williamsf1e5d692016-03-30 15:21:19 -05001837 LOG_LEVEL = (
1838 (INFO, "info"),
1839 (WARNING, "warn"),
1840 (ERROR, "error"),
1841 (CRITICAL, "critical"),
1842 (EXCEPTION, "toaster exception")
1843 )
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001844
1845 build = models.ForeignKey(Build)
1846 task = models.ForeignKey(Task, blank = True, null=True)
1847 level = models.IntegerField(choices=LOG_LEVEL, default=INFO)
Patrick Williamsf1e5d692016-03-30 15:21:19 -05001848 message = models.TextField(blank=True, null=True)
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001849 pathname = models.FilePathField(max_length=255, blank=True)
1850 lineno = models.IntegerField(null=True)
1851
1852 def __str__(self):
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001853 return force_bytes('%s %s %s' % (self.get_level_display(), self.message, self.build))
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001854
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
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001862def signal_runbuilds():
1863 """Send SIGUSR1 to runbuilds process"""
Brad Bishop6e60e8b2018-02-01 10:27:11 -05001864 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")
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001870
Brad Bishopd7bf8c12018-02-25 22:55:05 -05001871class 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):
Brad Bishop1a4b7ee2018-12-16 17:11:34 -08001880 path = 'conf/distro/%s.conf' % self.name
Brad Bishopd7bf8c12018-02-25 22:55:05 -05001881 return self.layer_version.get_vcs_file_link_url(path)
1882
1883 def __unicode__(self):
1884 return "Distro " + self.name + "(" + self.description + ")"
1885
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001886django.db.models.signals.post_save.connect(invalidate_cache)
1887django.db.models.signals.post_delete.connect(invalidate_cache)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001888django.db.models.signals.m2m_changed.connect(invalidate_cache)