blob: a7de57c256170f6e5e9250373fdf0e04f3bb4fa3 [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
41import logging
42logger = logging.getLogger("toaster")
43
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050044if 'sqlite' in settings.DATABASES['default']['ENGINE']:
45 from django.db import transaction, OperationalError
46 from time import sleep
47
48 _base_save = models.Model.save
49 def save(self, *args, **kwargs):
50 while True:
51 try:
52 with transaction.atomic():
53 return _base_save(self, *args, **kwargs)
54 except OperationalError as err:
55 if 'database is locked' in str(err):
56 logger.warning("%s, model: %s, args: %s, kwargs: %s",
57 err, self.__class__, args, kwargs)
58 sleep(0.5)
59 continue
60 raise
61
62 models.Model.save = save
63
64 # HACK: Monkey patch Django to fix 'database is locked' issue
65
66 from django.db.models.query import QuerySet
67 _base_insert = QuerySet._insert
68 def _insert(self, *args, **kwargs):
69 with transaction.atomic(using=self.db, savepoint=False):
70 return _base_insert(self, *args, **kwargs)
71 QuerySet._insert = _insert
72
73 from django.utils import six
74 def _create_object_from_params(self, lookup, params):
75 """
76 Tries to create an object using passed params.
77 Used by get_or_create and update_or_create
78 """
79 try:
80 obj = self.create(**params)
81 return obj, True
Patrick Williamsc0f7c042017-02-23 20:41:17 -060082 except (IntegrityError, DataError):
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050083 exc_info = sys.exc_info()
84 try:
85 return self.get(**lookup), False
86 except self.model.DoesNotExist:
87 pass
88 six.reraise(*exc_info)
89
90 QuerySet._create_object_from_params = _create_object_from_params
91
92 # end of HACK
Patrick Williamsc124f4f2015-09-15 14:41:29 -050093
94class GitURLValidator(validators.URLValidator):
95 import re
96 regex = re.compile(
97 r'^(?:ssh|git|http|ftp)s?://' # http:// or https://
98 r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|' # domain...
99 r'localhost|' # localhost...
100 r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|' # ...or ipv4
101 r'\[?[A-F0-9]*:[A-F0-9:]+\]?)' # ...or ipv6
102 r'(?::\d+)?' # optional port
103 r'(?:/?|[/?]\S+)$', re.IGNORECASE)
104
105def GitURLField(**kwargs):
106 r = models.URLField(**kwargs)
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600107 for i in range(len(r.validators)):
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500108 if isinstance(r.validators[i], validators.URLValidator):
109 r.validators[i] = GitURLValidator()
110 return r
111
112
113class ToasterSetting(models.Model):
114 name = models.CharField(max_length=63)
115 helptext = models.TextField()
116 value = models.CharField(max_length=255)
117
118 def __unicode__(self):
119 return "Setting %s = %s" % (self.name, self.value)
120
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600121
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500122class ProjectManager(models.Manager):
123 def create_project(self, name, release):
124 if release is not None:
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600125 prj = self.model(name=name,
126 bitbake_version=release.bitbake_version,
127 release=release)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500128 else:
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600129 prj = self.model(name=name,
130 bitbake_version=None,
131 release=None)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500132
133 prj.save()
134
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600135 for defaultconf in ToasterSetting.objects.filter(
136 name__startswith="DEFCONF_"):
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500137 name = defaultconf.name[8:]
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600138 ProjectVariable.objects.create(project=prj,
139 name=name,
140 value=defaultconf.value)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500141
142 if release is None:
143 return prj
144
145 for rdl in release.releasedefaultlayer_set.all():
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600146 lv = Layer_Version.objects.filter(
147 layer__name=rdl.layer_name,
148 release=release).first()
149
150 if lv:
151 ProjectLayer.objects.create(project=prj,
152 layercommit=lv,
153 optional=False)
154 else:
155 logger.warning("Default project layer %s not found" %
156 rdl.layer_name)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500157
158 return prj
159
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500160 # return single object with is_default = True
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500161 def get_or_create_default_project(self):
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600162 projects = super(ProjectManager, self).filter(is_default=True)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500163
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500164 if len(projects) > 1:
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500165 raise Exception('Inconsistent project data: multiple ' +
166 'default projects (i.e. with is_default=True)')
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500167 elif len(projects) < 1:
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500168 options = {
169 'name': 'Command line builds',
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600170 'short_description':
171 'Project for builds started outside Toaster',
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500172 'is_default': True
173 }
174 project = Project.objects.create(**options)
175 project.save()
176
177 return project
178 else:
179 return projects[0]
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500180
181class Project(models.Model):
182 search_allowed_fields = ['name', 'short_description', 'release__name', 'release__branch_name']
183 name = models.CharField(max_length=100)
184 short_description = models.CharField(max_length=50, blank=True)
185 bitbake_version = models.ForeignKey('BitbakeVersion', null=True)
186 release = models.ForeignKey("Release", null=True)
187 created = models.DateTimeField(auto_now_add = True)
188 updated = models.DateTimeField(auto_now = True)
189 # This is a horrible hack; since Toaster has no "User" model available when
190 # running in interactive mode, we can't reference the field here directly
191 # Instead, we keep a possible null reference to the User id, as not to force
192 # hard links to possibly missing models
193 user_id = models.IntegerField(null = True)
194 objects = ProjectManager()
195
196 # set to True for the project which is the default container
197 # for builds initiated by the command line etc.
198 is_default = models.BooleanField(default = False)
199
200 def __unicode__(self):
201 return "%s (Release %s, BBV %s)" % (self.name, self.release, self.bitbake_version)
202
203 def get_current_machine_name(self):
204 try:
205 return self.projectvariable_set.get(name="MACHINE").value
206 except (ProjectVariable.DoesNotExist,IndexError):
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500207 return None;
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500208
209 def get_number_of_builds(self):
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500210 """Return the number of builds which have ended"""
211
212 return self.build_set.exclude(
213 Q(outcome=Build.IN_PROGRESS) |
214 Q(outcome=Build.CANCELLED)
215 ).count()
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500216
217 def get_last_build_id(self):
218 try:
219 return Build.objects.filter( project = self.id ).order_by('-completed_on')[0].id
220 except (Build.DoesNotExist,IndexError):
221 return( -1 )
222
223 def get_last_outcome(self):
224 build_id = self.get_last_build_id
225 if (-1 == build_id):
226 return( "" )
227 try:
228 return Build.objects.filter( id = self.get_last_build_id )[ 0 ].outcome
229 except (Build.DoesNotExist,IndexError):
230 return( "not_found" )
231
232 def get_last_target(self):
233 build_id = self.get_last_build_id
234 if (-1 == build_id):
235 return( "" )
236 try:
237 return Target.objects.filter(build = build_id)[0].target
238 except (Target.DoesNotExist,IndexError):
239 return( "not_found" )
240
241 def get_last_errors(self):
242 build_id = self.get_last_build_id
243 if (-1 == build_id):
244 return( 0 )
245 try:
246 return Build.objects.filter(id = build_id)[ 0 ].errors.count()
247 except (Build.DoesNotExist,IndexError):
248 return( "not_found" )
249
250 def get_last_warnings(self):
251 build_id = self.get_last_build_id
252 if (-1 == build_id):
253 return( 0 )
254 try:
255 return Build.objects.filter(id = build_id)[ 0 ].warnings.count()
256 except (Build.DoesNotExist,IndexError):
257 return( "not_found" )
258
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500259 def get_last_build_extensions(self):
260 """
261 Get list of file name extensions for images produced by the most
262 recent build
263 """
264 last_build = Build.objects.get(pk = self.get_last_build_id())
265 return last_build.get_image_file_extensions()
266
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500267 def get_last_imgfiles(self):
268 build_id = self.get_last_build_id
269 if (-1 == build_id):
270 return( "" )
271 try:
272 return Variable.objects.filter(build = build_id, variable_name = "IMAGE_FSTYPES")[ 0 ].variable_value
273 except (Variable.DoesNotExist,IndexError):
274 return( "not_found" )
275
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500276 def get_all_compatible_layer_versions(self):
277 """ Returns Queryset of all Layer_Versions which are compatible with
278 this project"""
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500279 queryset = None
280
281 # guard on release, as it can be null
282 if self.release:
283 queryset = Layer_Version.objects.filter(
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600284 (Q(release=self.release) &
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500285 Q(build=None) &
286 Q(project=None)) |
287 Q(project=self))
288 else:
289 queryset = Layer_Version.objects.none()
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500290
291 return queryset
292
293 def get_project_layer_versions(self, pk=False):
294 """ Returns the Layer_Versions currently added to this project """
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500295 layer_versions = self.projectlayer_set.all().values_list('layercommit',
296 flat=True)
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500297
298 if pk is False:
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500299 return Layer_Version.objects.filter(pk__in=layer_versions)
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500300 else:
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500301 return layer_versions
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500302
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500303
304 def get_available_machines(self):
305 """ Returns QuerySet of all Machines which are provided by the
306 Layers currently added to the Project """
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500307 queryset = Machine.objects.filter(
308 layer_version__in=self.get_project_layer_versions())
309
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500310 return queryset
311
312 def get_all_compatible_machines(self):
313 """ Returns QuerySet of all the compatible machines available to the
314 project including ones from Layers not currently added """
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500315 queryset = Machine.objects.filter(
316 layer_version__in=self.get_all_compatible_layer_versions())
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500317
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500318 return queryset
319
320 def get_available_recipes(self):
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500321 """ Returns QuerySet of all the recipes that are provided by layers
322 added to this project """
323 queryset = Recipe.objects.filter(
324 layer_version__in=self.get_project_layer_versions())
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500325
326 return queryset
327
328 def get_all_compatible_recipes(self):
329 """ Returns QuerySet of all the compatible Recipes available to the
330 project including ones from Layers not currently added """
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500331 queryset = Recipe.objects.filter(
332 layer_version__in=self.get_all_compatible_layer_versions()).exclude(name__exact='')
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500333
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500334 return queryset
335
336
337 def schedule_build(self):
338 from bldcontrol.models import BuildRequest, BRTarget, BRLayer, BRVariable, BRBitbake
339 br = BuildRequest.objects.create(project = self)
340 try:
341
342 BRBitbake.objects.create(req = br,
343 giturl = self.bitbake_version.giturl,
344 commit = self.bitbake_version.branch,
345 dirpath = self.bitbake_version.dirpath)
346
347 for l in self.projectlayer_set.all().order_by("pk"):
348 commit = l.layercommit.get_vcs_reference()
349 print("ii Building layer ", l.layercommit.layer.name, " at vcs point ", commit)
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600350 BRLayer.objects.create(
351 req=br,
352 name=l.layercommit.layer.name,
353 giturl=l.layercommit.layer.vcs_url,
354 commit=commit,
355 dirpath=l.layercommit.dirpath,
356 layer_version=l.layercommit,
357 local_source_dir=l.layercommit.layer.local_source_dir
358 )
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500359
360 br.state = BuildRequest.REQ_QUEUED
361 now = timezone.now()
362 br.build = Build.objects.create(project = self,
363 completed_on=now,
364 started_on=now,
365 )
366 for t in self.projecttarget_set.all():
367 BRTarget.objects.create(req = br, target = t.target, task = t.task)
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500368 Target.objects.create(build = br.build, target = t.target, task = t.task)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500369
370 for v in self.projectvariable_set.all():
371 BRVariable.objects.create(req = br, name = v.name, value = v.value)
372
373
374 try:
375 br.build.machine = self.projectvariable_set.get(name = 'MACHINE').value
376 br.build.save()
377 except ProjectVariable.DoesNotExist:
378 pass
379 br.save()
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600380 signal_runbuilds()
381
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500382 except Exception:
383 # revert the build request creation since we're not done cleanly
384 br.delete()
385 raise
386 return br
387
388class Build(models.Model):
389 SUCCEEDED = 0
390 FAILED = 1
391 IN_PROGRESS = 2
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500392 CANCELLED = 3
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500393
394 BUILD_OUTCOME = (
395 (SUCCEEDED, 'Succeeded'),
396 (FAILED, 'Failed'),
397 (IN_PROGRESS, 'In Progress'),
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500398 (CANCELLED, 'Cancelled'),
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500399 )
400
401 search_allowed_fields = ['machine', 'cooker_log_path', "target__target", "target__target_image_file__file_name"]
402
403 project = models.ForeignKey(Project) # must have a project
404 machine = models.CharField(max_length=100)
405 distro = models.CharField(max_length=100)
406 distro_version = models.CharField(max_length=100)
407 started_on = models.DateTimeField()
408 completed_on = models.DateTimeField()
409 outcome = models.IntegerField(choices=BUILD_OUTCOME, default=IN_PROGRESS)
410 cooker_log_path = models.CharField(max_length=500)
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600411 build_name = models.CharField(max_length=100, default='')
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500412 bitbake_version = models.CharField(max_length=50)
413
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600414 # number of recipes to parse for this build
415 recipes_to_parse = models.IntegerField(default=1)
416
417 # number of recipes parsed so far for this build
418 recipes_parsed = models.IntegerField(default=0)
419
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500420 @staticmethod
421 def get_recent(project=None):
422 """
423 Return recent builds as a list; if project is set, only return
424 builds for that project
425 """
426
427 builds = Build.objects.all()
428
429 if project:
430 builds = builds.filter(project=project)
431
432 finished_criteria = \
433 Q(outcome=Build.SUCCEEDED) | \
434 Q(outcome=Build.FAILED) | \
435 Q(outcome=Build.CANCELLED)
436
437 recent_builds = list(itertools.chain(
438 builds.filter(outcome=Build.IN_PROGRESS).order_by("-started_on"),
439 builds.filter(finished_criteria).order_by("-completed_on")[:3]
440 ))
441
442 # add percentage done property to each build; this is used
443 # to show build progress in mrb_section.html
444 for build in recent_builds:
445 build.percentDone = build.completeper()
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600446 build.outcomeText = build.get_outcome_text()
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500447
448 return recent_builds
449
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600450 def started(self):
451 """
452 As build variables are only added for a build when its BuildStarted event
453 is received, a build with no build variables is counted as
454 "in preparation" and not properly started yet. This method
455 will return False if a build has no build variables (it never properly
456 started), or True otherwise.
457
458 Note that this is a temporary workaround for the fact that we don't
459 have a fine-grained state variable on a build which would allow us
460 to record "in progress" (BuildStarted received) vs. "in preparation".
461 """
462 variables = Variable.objects.filter(build=self)
463 return len(variables) > 0
464
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500465 def completeper(self):
466 tf = Task.objects.filter(build = self)
467 tfc = tf.count()
468 if tfc > 0:
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600469 completeper = tf.exclude(order__isnull=True).count()*100 // tfc
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500470 else:
471 completeper = 0
472 return completeper
473
474 def eta(self):
475 eta = timezone.now()
476 completeper = self.completeper()
477 if self.completeper() > 0:
478 eta += ((eta - self.started_on)*(100-completeper))/completeper
479 return eta
480
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600481 def has_images(self):
482 """
483 Returns True if at least one of the targets for this build has an
484 image file associated with it, False otherwise
485 """
486 targets = Target.objects.filter(build_id=self.id)
487 has_images = False
488 for target in targets:
489 if target.has_images():
490 has_images = True
491 break
492 return has_images
493
494 def has_image_recipes(self):
495 """
496 Returns True if a build has any targets which were built from
497 image recipes.
498 """
499 image_recipes = self.get_image_recipes()
500 return len(image_recipes) > 0
501
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500502 def get_image_file_extensions(self):
503 """
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600504 Get string of file name extensions for images produced by this build;
505 note that this is the actual list of extensions stored on Target objects
506 for this build, and not the value of IMAGE_FSTYPES.
507
508 Returns comma-separated string, e.g. "vmdk, ext4"
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500509 """
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500510 extensions = []
511
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600512 targets = Target.objects.filter(build_id = self.id)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500513 for target in targets:
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600514 if not target.is_image:
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500515 continue
516
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600517 target_image_files = Target_Image_File.objects.filter(
518 target_id=target.id)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500519
520 for target_image_file in target_image_files:
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600521 extensions.append(target_image_file.suffix)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500522
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600523 extensions = list(set(extensions))
524 extensions.sort()
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500525
526 return ', '.join(extensions)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500527
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600528 def get_image_fstypes(self):
529 """
530 Get the IMAGE_FSTYPES variable value for this build as a de-duplicated
531 list of image file suffixes.
532 """
533 image_fstypes = Variable.objects.get(
534 build=self, variable_name='IMAGE_FSTYPES').variable_value
535 return list(set(re.split(r' {1,}', image_fstypes)))
536
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500537 def get_sorted_target_list(self):
538 tgts = Target.objects.filter(build_id = self.id).order_by( 'target' );
539 return( tgts );
540
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500541 def get_recipes(self):
542 """
543 Get the recipes related to this build;
544 note that the related layer versions and layers are also prefetched
545 by this query, as this queryset can be sorted by these objects in the
546 build recipes view; prefetching them here removes the need
547 for another query in that view
548 """
549 layer_versions = Layer_Version.objects.filter(build=self)
550 criteria = Q(layer_version__id__in=layer_versions)
551 return Recipe.objects.filter(criteria) \
552 .select_related('layer_version', 'layer_version__layer')
553
554 def get_image_recipes(self):
555 """
556 Returns a list of image Recipes (custom and built-in) related to this
557 build, sorted by name; note that this has to be done in two steps, as
558 there's no way to get all the custom image recipes and image recipes
559 in one query
560 """
561 custom_image_recipes = self.get_custom_image_recipes()
562 custom_image_recipe_names = custom_image_recipes.values_list('name', flat=True)
563
564 not_custom_image_recipes = ~Q(name__in=custom_image_recipe_names) & \
565 Q(is_image=True)
566
567 built_image_recipes = self.get_recipes().filter(not_custom_image_recipes)
568
569 # append to the custom image recipes and sort
570 customisable_image_recipes = list(
571 itertools.chain(custom_image_recipes, built_image_recipes)
572 )
573
574 return sorted(customisable_image_recipes, key=lambda recipe: recipe.name)
575
576 def get_custom_image_recipes(self):
577 """
578 Returns a queryset of CustomImageRecipes related to this build,
579 sorted by name
580 """
581 built_recipe_names = self.get_recipes().values_list('name', flat=True)
582 criteria = Q(name__in=built_recipe_names) & Q(project=self.project)
583 queryset = CustomImageRecipe.objects.filter(criteria).order_by('name')
584 return queryset
585
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500586 def get_outcome_text(self):
587 return Build.BUILD_OUTCOME[int(self.outcome)][1]
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500588
589 @property
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500590 def failed_tasks(self):
591 """ Get failed tasks for the build """
592 tasks = self.task_build.all()
593 return tasks.filter(order__gt=0, outcome=Task.OUTCOME_FAILED)
594
595 @property
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500596 def errors(self):
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500597 return (self.logmessage_set.filter(level=LogMessage.ERROR) |
598 self.logmessage_set.filter(level=LogMessage.EXCEPTION) |
599 self.logmessage_set.filter(level=LogMessage.CRITICAL))
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500600
601 @property
602 def warnings(self):
603 return self.logmessage_set.filter(level=LogMessage.WARNING)
604
605 @property
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500606 def timespent(self):
607 return self.completed_on - self.started_on
608
609 @property
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500610 def timespent_seconds(self):
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500611 return self.timespent.total_seconds()
612
613 @property
614 def target_labels(self):
615 """
616 Sorted (a-z) "target1:task, target2, target3" etc. string for all
617 targets in this build
618 """
619 targets = self.target_set.all()
620 target_labels = [target.target +
621 (':' + target.task if target.task else '')
622 for target in targets]
623 target_labels.sort()
624
625 return target_labels
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500626
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600627 def get_buildrequest(self):
628 buildrequest = None
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500629 if hasattr(self, 'buildrequest'):
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600630 buildrequest = self.buildrequest
631 return buildrequest
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500632
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600633 def is_queued(self):
634 from bldcontrol.models import BuildRequest
635 buildrequest = self.get_buildrequest()
636 if buildrequest:
637 return buildrequest.state == BuildRequest.REQ_QUEUED
638 else:
639 return False
640
641 def is_cancelling(self):
642 from bldcontrol.models import BuildRequest
643 buildrequest = self.get_buildrequest()
644 if buildrequest:
645 return self.outcome == Build.IN_PROGRESS and \
646 buildrequest.state == BuildRequest.REQ_CANCELLING
647 else:
648 return False
649
650 def is_parsing(self):
651 """
652 True if the build is still parsing recipes
653 """
654 return self.outcome == Build.IN_PROGRESS and \
655 self.recipes_parsed < self.recipes_to_parse
656
657 def is_starting(self):
658 """
659 True if the build has no completed tasks yet and is still just starting
660 tasks.
661
662 Note that the mechanism for testing whether a Task is "done" is whether
663 its order field is set, as per the completeper() method.
664 """
665 return self.outcome == Build.IN_PROGRESS and \
666 self.task_build.filter(order__isnull=False).count() == 0
667
668 def get_state(self):
669 """
670 Get the state of the build; one of 'Succeeded', 'Failed', 'In Progress',
671 'Cancelled' (Build outcomes); or 'Queued', 'Cancelling' (states
672 dependent on the BuildRequest state).
673
674 This works around the fact that we have BuildRequest states as well
675 as Build states, but really we just want to know the state of the build.
676 """
677 if self.is_cancelling():
678 return 'Cancelling';
679 elif self.is_queued():
680 return 'Queued'
681 elif self.is_parsing():
682 return 'Parsing'
683 elif self.is_starting():
684 return 'Starting'
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500685 else:
686 return self.get_outcome_text()
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500687
688 def __str__(self):
689 return "%d %s %s" % (self.id, self.project, ",".join([t.target for t in self.target_set.all()]))
690
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500691class ProjectTarget(models.Model):
692 project = models.ForeignKey(Project)
693 target = models.CharField(max_length=100)
694 task = models.CharField(max_length=100, null=True)
695
696class Target(models.Model):
697 search_allowed_fields = ['target', 'file_name']
698 build = models.ForeignKey(Build)
699 target = models.CharField(max_length=100)
700 task = models.CharField(max_length=100, null=True)
701 is_image = models.BooleanField(default = False)
702 image_size = models.IntegerField(default=0)
703 license_manifest_path = models.CharField(max_length=500, null=True)
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600704 package_manifest_path = models.CharField(max_length=500, null=True)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500705
706 def package_count(self):
707 return Target_Installed_Package.objects.filter(target_id__exact=self.id).count()
708
709 def __unicode__(self):
710 return self.target
711
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600712 def get_similar_targets(self):
713 """
714 Get target sfor the same machine, task and target name
715 (e.g. 'core-image-minimal') from a successful build for this project
716 (but excluding this target).
717
718 Note that we only look for targets built by this project because
719 projects can have different configurations from each other, and put
720 their artifacts in different directories.
721
722 The possibility of error when retrieving candidate targets
723 is minimised by the fact that bitbake will rebuild artifacts if MACHINE
724 (or various other variables) change. In this case, there is no need to
725 clone artifacts from another target, as those artifacts will have
726 been re-generated for this target anyway.
727 """
728 query = ~Q(pk=self.pk) & \
729 Q(target=self.target) & \
730 Q(build__machine=self.build.machine) & \
731 Q(build__outcome=Build.SUCCEEDED) & \
732 Q(build__project=self.build.project)
733
734 return Target.objects.filter(query)
735
736 def get_similar_target_with_image_files(self):
737 """
738 Get the most recent similar target with Target_Image_Files associated
739 with it, for the purpose of cloning those files onto this target.
740 """
741 similar_target = None
742
743 candidates = self.get_similar_targets()
744 if candidates.count() == 0:
745 return similar_target
746
747 task_subquery = Q(task=self.task)
748
749 # we can look for a 'build' task if this task is a 'populate_sdk_ext'
750 # task, as the latter also creates images; and vice versa; note that
751 # 'build' targets can have their task set to '';
752 # also note that 'populate_sdk' does not produce image files
753 image_tasks = [
754 '', # aka 'build'
755 'build',
756 'image',
757 'populate_sdk_ext'
758 ]
759 if self.task in image_tasks:
760 task_subquery = Q(task__in=image_tasks)
761
762 # annotate with the count of files, to exclude any targets which
763 # don't have associated files
764 candidates = candidates.annotate(num_files=Count('target_image_file'))
765
766 query = task_subquery & Q(num_files__gt=0)
767
768 candidates = candidates.filter(query)
769
770 if candidates.count() > 0:
771 candidates.order_by('build__completed_on')
772 similar_target = candidates.last()
773
774 return similar_target
775
776 def get_similar_target_with_sdk_files(self):
777 """
778 Get the most recent similar target with TargetSDKFiles associated
779 with it, for the purpose of cloning those files onto this target.
780 """
781 similar_target = None
782
783 candidates = self.get_similar_targets()
784 if candidates.count() == 0:
785 return similar_target
786
787 # annotate with the count of files, to exclude any targets which
788 # don't have associated files
789 candidates = candidates.annotate(num_files=Count('targetsdkfile'))
790
791 query = Q(task=self.task) & Q(num_files__gt=0)
792
793 candidates = candidates.filter(query)
794
795 if candidates.count() > 0:
796 candidates.order_by('build__completed_on')
797 similar_target = candidates.last()
798
799 return similar_target
800
801 def clone_image_artifacts_from(self, target):
802 """
803 Make clones of the Target_Image_Files and TargetKernelFile objects
804 associated with Target target, then associate them with this target.
805
806 Note that for Target_Image_Files, we only want files from the previous
807 build whose suffix matches one of the suffixes defined in this
808 target's build's IMAGE_FSTYPES configuration variable. This prevents the
809 Target_Image_File object for an ext4 image being associated with a
810 target for a project which didn't produce an ext4 image (for example).
811
812 Also sets the license_manifest_path and package_manifest_path
813 of this target to the same path as that of target being cloned from, as
814 the manifests are also build artifacts but are treated differently.
815 """
816
817 image_fstypes = self.build.get_image_fstypes()
818
819 # filter out any image files whose suffixes aren't in the
820 # IMAGE_FSTYPES suffixes variable for this target's build
821 image_files = [target_image_file \
822 for target_image_file in target.target_image_file_set.all() \
823 if target_image_file.suffix in image_fstypes]
824
825 for image_file in image_files:
826 image_file.pk = None
827 image_file.target = self
828 image_file.save()
829
830 kernel_files = target.targetkernelfile_set.all()
831 for kernel_file in kernel_files:
832 kernel_file.pk = None
833 kernel_file.target = self
834 kernel_file.save()
835
836 self.license_manifest_path = target.license_manifest_path
837 self.package_manifest_path = target.package_manifest_path
838 self.save()
839
840 def clone_sdk_artifacts_from(self, target):
841 """
842 Clone TargetSDKFile objects from target and associate them with this
843 target.
844 """
845 sdk_files = target.targetsdkfile_set.all()
846 for sdk_file in sdk_files:
847 sdk_file.pk = None
848 sdk_file.target = self
849 sdk_file.save()
850
851 def has_images(self):
852 """
853 Returns True if this target has one or more image files attached to it.
854 """
855 return self.target_image_file_set.all().count() > 0
856
857# kernel artifacts for a target: bzImage and modules*
858class TargetKernelFile(models.Model):
859 target = models.ForeignKey(Target)
860 file_name = models.FilePathField()
861 file_size = models.IntegerField()
862
863 @property
864 def basename(self):
865 return os.path.basename(self.file_name)
866
867# SDK artifacts for a target: sh and manifest files
868class TargetSDKFile(models.Model):
869 target = models.ForeignKey(Target)
870 file_name = models.FilePathField()
871 file_size = models.IntegerField()
872
873 @property
874 def basename(self):
875 return os.path.basename(self.file_name)
876
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500877class Target_Image_File(models.Model):
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500878 # valid suffixes for image files produced by a build
879 SUFFIXES = {
880 'btrfs', 'cpio', 'cpio.gz', 'cpio.lz4', 'cpio.lzma', 'cpio.xz',
881 'cramfs', 'elf', 'ext2', 'ext2.bz2', 'ext2.gz', 'ext2.lzma', 'ext4',
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600882 'ext4.gz', 'ext3', 'ext3.gz', 'hdddirect', 'hddimg', 'iso', 'jffs2',
883 'jffs2.sum', 'multiubi', 'qcow2', 'squashfs', 'squashfs-lzo',
884 'squashfs-xz', 'tar', 'tar.bz2', 'tar.gz', 'tar.lz4', 'tar.xz', 'ubi',
885 'ubifs', 'vdi', 'vmdk', 'wic', 'wic.bz2', 'wic.gz', 'wic.lzma'
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500886 }
887
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500888 target = models.ForeignKey(Target)
889 file_name = models.FilePathField(max_length=254)
890 file_size = models.IntegerField()
891
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500892 @property
893 def suffix(self):
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600894 """
895 Suffix for image file, minus leading "."
896 """
897 for suffix in Target_Image_File.SUFFIXES:
898 if self.file_name.endswith(suffix):
899 return suffix
900
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500901 filename, suffix = os.path.splitext(self.file_name)
902 suffix = suffix.lstrip('.')
903 return suffix
904
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500905class Target_File(models.Model):
906 ITYPE_REGULAR = 1
907 ITYPE_DIRECTORY = 2
908 ITYPE_SYMLINK = 3
909 ITYPE_SOCKET = 4
910 ITYPE_FIFO = 5
911 ITYPE_CHARACTER = 6
912 ITYPE_BLOCK = 7
913 ITYPES = ( (ITYPE_REGULAR ,'regular'),
914 ( ITYPE_DIRECTORY ,'directory'),
915 ( ITYPE_SYMLINK ,'symlink'),
916 ( ITYPE_SOCKET ,'socket'),
917 ( ITYPE_FIFO ,'fifo'),
918 ( ITYPE_CHARACTER ,'character'),
919 ( ITYPE_BLOCK ,'block'),
920 )
921
922 target = models.ForeignKey(Target)
923 path = models.FilePathField()
924 size = models.IntegerField()
925 inodetype = models.IntegerField(choices = ITYPES)
926 permission = models.CharField(max_length=16)
927 owner = models.CharField(max_length=128)
928 group = models.CharField(max_length=128)
929 directory = models.ForeignKey('Target_File', related_name="directory_set", null=True)
930 sym_target = models.ForeignKey('Target_File', related_name="symlink_set", null=True)
931
932
933class Task(models.Model):
934
935 SSTATE_NA = 0
936 SSTATE_MISS = 1
937 SSTATE_FAILED = 2
938 SSTATE_RESTORED = 3
939
940 SSTATE_RESULT = (
941 (SSTATE_NA, 'Not Applicable'), # For rest of tasks, but they still need checking.
942 (SSTATE_MISS, 'File not in cache'), # the sstate object was not found
943 (SSTATE_FAILED, 'Failed'), # there was a pkg, but the script failed
944 (SSTATE_RESTORED, 'Succeeded'), # successfully restored
945 )
946
947 CODING_NA = 0
948 CODING_PYTHON = 2
949 CODING_SHELL = 3
950
951 TASK_CODING = (
952 (CODING_NA, 'N/A'),
953 (CODING_PYTHON, 'Python'),
954 (CODING_SHELL, 'Shell'),
955 )
956
957 OUTCOME_NA = -1
958 OUTCOME_SUCCESS = 0
959 OUTCOME_COVERED = 1
960 OUTCOME_CACHED = 2
961 OUTCOME_PREBUILT = 3
962 OUTCOME_FAILED = 4
963 OUTCOME_EMPTY = 5
964
965 TASK_OUTCOME = (
966 (OUTCOME_NA, 'Not Available'),
967 (OUTCOME_SUCCESS, 'Succeeded'),
968 (OUTCOME_COVERED, 'Covered'),
969 (OUTCOME_CACHED, 'Cached'),
970 (OUTCOME_PREBUILT, 'Prebuilt'),
971 (OUTCOME_FAILED, 'Failed'),
972 (OUTCOME_EMPTY, 'Empty'),
973 )
974
975 TASK_OUTCOME_HELP = (
976 (OUTCOME_SUCCESS, 'This task successfully completed'),
977 (OUTCOME_COVERED, 'This task did not run because its output is provided by another task'),
978 (OUTCOME_CACHED, 'This task restored output from the sstate-cache directory or mirrors'),
979 (OUTCOME_PREBUILT, 'This task did not run because its outcome was reused from a previous build'),
980 (OUTCOME_FAILED, 'This task did not complete'),
981 (OUTCOME_EMPTY, 'This task has no executable content'),
982 (OUTCOME_NA, ''),
983 )
984
985 search_allowed_fields = [ "recipe__name", "recipe__version", "task_name", "logfile" ]
986
987 def __init__(self, *args, **kwargs):
988 super(Task, self).__init__(*args, **kwargs)
989 try:
990 self._helptext = HelpText.objects.get(key=self.task_name, area=HelpText.VARIABLE, build=self.build).text
991 except HelpText.DoesNotExist:
992 self._helptext = None
993
994 def get_related_setscene(self):
995 return Task.objects.filter(task_executed=True, build = self.build, recipe = self.recipe, task_name=self.task_name+"_setscene")
996
997 def get_outcome_text(self):
998 return Task.TASK_OUTCOME[int(self.outcome) + 1][1]
999
1000 def get_outcome_help(self):
1001 return Task.TASK_OUTCOME_HELP[int(self.outcome)][1]
1002
1003 def get_sstate_text(self):
1004 if self.sstate_result==Task.SSTATE_NA:
1005 return ''
1006 else:
1007 return Task.SSTATE_RESULT[int(self.sstate_result)][1]
1008
1009 def get_executed_display(self):
1010 if self.task_executed:
1011 return "Executed"
1012 return "Not Executed"
1013
1014 def get_description(self):
1015 return self._helptext
1016
1017 build = models.ForeignKey(Build, related_name='task_build')
1018 order = models.IntegerField(null=True)
1019 task_executed = models.BooleanField(default=False) # True means Executed, False means Not/Executed
1020 outcome = models.IntegerField(choices=TASK_OUTCOME, default=OUTCOME_NA)
1021 sstate_checksum = models.CharField(max_length=100, blank=True)
1022 path_to_sstate_obj = models.FilePathField(max_length=500, blank=True)
1023 recipe = models.ForeignKey('Recipe', related_name='tasks')
1024 task_name = models.CharField(max_length=100)
1025 source_url = models.FilePathField(max_length=255, blank=True)
1026 work_directory = models.FilePathField(max_length=255, blank=True)
1027 script_type = models.IntegerField(choices=TASK_CODING, default=CODING_NA)
1028 line_number = models.IntegerField(default=0)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001029
1030 # start/end times
1031 started = models.DateTimeField(null=True)
1032 ended = models.DateTimeField(null=True)
1033
1034 # in seconds; this is stored to enable sorting
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001035 elapsed_time = models.DecimalField(max_digits=8, decimal_places=2, null=True)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001036
1037 # in bytes; note that disk_io is stored to enable sorting
1038 disk_io = models.IntegerField(null=True)
1039 disk_io_read = models.IntegerField(null=True)
1040 disk_io_write = models.IntegerField(null=True)
1041
1042 # in seconds
1043 cpu_time_user = models.DecimalField(max_digits=8, decimal_places=2, null=True)
1044 cpu_time_system = models.DecimalField(max_digits=8, decimal_places=2, null=True)
1045
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001046 sstate_result = models.IntegerField(choices=SSTATE_RESULT, default=SSTATE_NA)
1047 message = models.CharField(max_length=240)
1048 logfile = models.FilePathField(max_length=255, blank=True)
1049
1050 outcome_text = property(get_outcome_text)
1051 sstate_text = property(get_sstate_text)
1052
1053 def __unicode__(self):
1054 return "%d(%d) %s:%s" % (self.pk, self.build.pk, self.recipe.name, self.task_name)
1055
1056 class Meta:
1057 ordering = ('order', 'recipe' ,)
1058 unique_together = ('build', 'recipe', 'task_name', )
1059
1060
1061class Task_Dependency(models.Model):
1062 task = models.ForeignKey(Task, related_name='task_dependencies_task')
1063 depends_on = models.ForeignKey(Task, related_name='task_dependencies_depends')
1064
1065class Package(models.Model):
1066 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 -05001067 build = models.ForeignKey('Build', null=True)
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001068 recipe = models.ForeignKey('Recipe', null=True)
1069 name = models.CharField(max_length=100)
1070 installed_name = models.CharField(max_length=100, default='')
1071 version = models.CharField(max_length=100, blank=True)
1072 revision = models.CharField(max_length=32, blank=True)
1073 summary = models.TextField(blank=True)
1074 description = models.TextField(blank=True)
1075 size = models.IntegerField(default=0)
1076 installed_size = models.IntegerField(default=0)
1077 section = models.CharField(max_length=80, blank=True)
1078 license = models.CharField(max_length=80, blank=True)
1079
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001080 @property
1081 def is_locale_package(self):
1082 """ Returns True if this package is identifiable as a locale package """
1083 if self.name.find('locale') != -1:
1084 return True
1085 return False
1086
1087 @property
1088 def is_packagegroup(self):
1089 """ Returns True is this package is identifiable as a packagegroup """
1090 if self.name.find('packagegroup') != -1:
1091 return True
1092 return False
1093
1094class CustomImagePackage(Package):
1095 # CustomImageRecipe fields to track pacakges appended,
1096 # included and excluded from a CustomImageRecipe
1097 recipe_includes = models.ManyToManyField('CustomImageRecipe',
1098 related_name='includes_set')
1099 recipe_excludes = models.ManyToManyField('CustomImageRecipe',
1100 related_name='excludes_set')
1101 recipe_appends = models.ManyToManyField('CustomImageRecipe',
1102 related_name='appends_set')
1103
1104
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001105class Package_DependencyManager(models.Manager):
1106 use_for_related_fields = True
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001107 TARGET_LATEST = "use-latest-target-for-target"
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001108
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001109 def get_queryset(self):
1110 return super(Package_DependencyManager, self).get_queryset().exclude(package_id = F('depends_on__id'))
1111
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001112 def for_target_or_none(self, target):
1113 """ filter the dependencies to be displayed by the supplied target
1114 if no dependences are found for the target then try None as the target
1115 which will return the dependences calculated without the context of a
1116 target e.g. non image recipes.
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001117
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001118 returns: { size, packages }
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001119 """
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001120 package_dependencies = self.all_depends().order_by('depends_on__name')
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001121
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001122 if target is self.TARGET_LATEST:
1123 installed_deps =\
1124 package_dependencies.filter(~Q(target__target=None))
1125 else:
1126 installed_deps =\
1127 package_dependencies.filter(Q(target__target=target))
1128
1129 packages_list = None
1130 total_size = 0
1131
1132 # If we have installed depdencies for this package and target then use
1133 # these to display
1134 if installed_deps.count() > 0:
1135 packages_list = installed_deps
1136 total_size = installed_deps.aggregate(
1137 Sum('depends_on__size'))['depends_on__size__sum']
1138 else:
1139 new_list = []
1140 package_names = []
1141
1142 # Find dependencies for the package that we know about even if
1143 # it's not installed on a target e.g. from a non-image recipe
1144 for p in package_dependencies.filter(Q(target=None)):
1145 if p.depends_on.name in package_names:
1146 continue
1147 else:
1148 package_names.append(p.depends_on.name)
1149 new_list.append(p.pk)
1150 # while we're here we may as well total up the size to
1151 # avoid iterating again
1152 total_size += p.depends_on.size
1153
1154 # We want to return a queryset here for consistency so pick the
1155 # deps from the new_list
1156 packages_list = package_dependencies.filter(Q(pk__in=new_list))
1157
1158 return {'packages': packages_list,
1159 'size': total_size}
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001160
1161 def all_depends(self):
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001162 """ Returns just the depends packages and not any other dep_type
1163 Note that this is for any target
1164 """
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001165 return self.filter(Q(dep_type=Package_Dependency.TYPE_RDEPENDS) |
1166 Q(dep_type=Package_Dependency.TYPE_TRDEPENDS))
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001167
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001168
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001169class Package_Dependency(models.Model):
1170 TYPE_RDEPENDS = 0
1171 TYPE_TRDEPENDS = 1
1172 TYPE_RRECOMMENDS = 2
1173 TYPE_TRECOMMENDS = 3
1174 TYPE_RSUGGESTS = 4
1175 TYPE_RPROVIDES = 5
1176 TYPE_RREPLACES = 6
1177 TYPE_RCONFLICTS = 7
1178 ' TODO: bpackage should be changed to remove the DEPENDS_TYPE access '
1179 DEPENDS_TYPE = (
1180 (TYPE_RDEPENDS, "depends"),
1181 (TYPE_TRDEPENDS, "depends"),
1182 (TYPE_TRECOMMENDS, "recommends"),
1183 (TYPE_RRECOMMENDS, "recommends"),
1184 (TYPE_RSUGGESTS, "suggests"),
1185 (TYPE_RPROVIDES, "provides"),
1186 (TYPE_RREPLACES, "replaces"),
1187 (TYPE_RCONFLICTS, "conflicts"),
1188 )
1189 """ Indexed by dep_type, in view order, key for short name and help
1190 description which when viewed will be printf'd with the
1191 package name.
1192 """
1193 DEPENDS_DICT = {
1194 TYPE_RDEPENDS : ("depends", "%s is required to run %s"),
1195 TYPE_TRDEPENDS : ("depends", "%s is required to run %s"),
1196 TYPE_TRECOMMENDS : ("recommends", "%s extends the usability of %s"),
1197 TYPE_RRECOMMENDS : ("recommends", "%s extends the usability of %s"),
1198 TYPE_RSUGGESTS : ("suggests", "%s is suggested for installation with %s"),
1199 TYPE_RPROVIDES : ("provides", "%s is provided by %s"),
1200 TYPE_RREPLACES : ("replaces", "%s is replaced by %s"),
1201 TYPE_RCONFLICTS : ("conflicts", "%s conflicts with %s, which will not be installed if this package is not first removed"),
1202 }
1203
1204 package = models.ForeignKey(Package, related_name='package_dependencies_source')
1205 depends_on = models.ForeignKey(Package, related_name='package_dependencies_target') # soft dependency
1206 dep_type = models.IntegerField(choices=DEPENDS_TYPE)
1207 target = models.ForeignKey(Target, null=True)
1208 objects = Package_DependencyManager()
1209
1210class Target_Installed_Package(models.Model):
1211 target = models.ForeignKey(Target)
1212 package = models.ForeignKey(Package, related_name='buildtargetlist_package')
1213
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001214
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001215class Package_File(models.Model):
1216 package = models.ForeignKey(Package, related_name='buildfilelist_package')
1217 path = models.FilePathField(max_length=255, blank=True)
1218 size = models.IntegerField()
1219
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001220
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001221class Recipe(models.Model):
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001222 search_allowed_fields = ['name', 'version', 'file_path', 'section',
1223 'summary', 'description', 'license',
1224 'layer_version__layer__name',
1225 'layer_version__branch', 'layer_version__commit',
1226 'layer_version__local_path',
1227 'layer_version__layer_source']
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001228
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001229 up_date = models.DateTimeField(null=True, default=None)
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001230
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001231 name = models.CharField(max_length=100, blank=True)
1232 version = models.CharField(max_length=100, blank=True)
1233 layer_version = models.ForeignKey('Layer_Version',
1234 related_name='recipe_layer_version')
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001235 summary = models.TextField(blank=True)
1236 description = models.TextField(blank=True)
1237 section = models.CharField(max_length=100, blank=True)
1238 license = models.CharField(max_length=200, blank=True)
1239 homepage = models.URLField(blank=True)
1240 bugtracker = models.URLField(blank=True)
1241 file_path = models.FilePathField(max_length=255)
1242 pathflags = models.CharField(max_length=200, blank=True)
1243 is_image = models.BooleanField(default=False)
1244
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001245 def __unicode__(self):
1246 return "Recipe " + self.name + ":" + self.version
1247
1248 def get_vcs_recipe_file_link_url(self):
1249 return self.layer_version.get_vcs_file_link_url(self.file_path)
1250
1251 def get_description_or_summary(self):
1252 if self.description:
1253 return self.description
1254 elif self.summary:
1255 return self.summary
1256 else:
1257 return ""
1258
1259 class Meta:
1260 unique_together = (("layer_version", "file_path", "pathflags"), )
1261
1262
1263class Recipe_DependencyManager(models.Manager):
1264 use_for_related_fields = True
1265
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001266 def get_queryset(self):
1267 return super(Recipe_DependencyManager, self).get_queryset().exclude(recipe_id = F('depends_on__id'))
1268
1269class Provides(models.Model):
1270 name = models.CharField(max_length=100)
1271 recipe = models.ForeignKey(Recipe)
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001272
1273class Recipe_Dependency(models.Model):
1274 TYPE_DEPENDS = 0
1275 TYPE_RDEPENDS = 1
1276
1277 DEPENDS_TYPE = (
1278 (TYPE_DEPENDS, "depends"),
1279 (TYPE_RDEPENDS, "rdepends"),
1280 )
1281 recipe = models.ForeignKey(Recipe, related_name='r_dependencies_recipe')
1282 depends_on = models.ForeignKey(Recipe, related_name='r_dependencies_depends')
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001283 via = models.ForeignKey(Provides, null=True, default=None)
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001284 dep_type = models.IntegerField(choices=DEPENDS_TYPE)
1285 objects = Recipe_DependencyManager()
1286
1287
1288class Machine(models.Model):
1289 search_allowed_fields = ["name", "description", "layer_version__layer__name"]
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001290 up_date = models.DateTimeField(null = True, default = None)
1291
1292 layer_version = models.ForeignKey('Layer_Version')
1293 name = models.CharField(max_length=255)
1294 description = models.CharField(max_length=255)
1295
1296 def get_vcs_machine_file_link_url(self):
1297 path = 'conf/machine/'+self.name+'.conf'
1298
1299 return self.layer_version.get_vcs_file_link_url(path)
1300
1301 def __unicode__(self):
1302 return "Machine " + self.name + "(" + self.description + ")"
1303
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001304
1305
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001306
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001307
1308class BitbakeVersion(models.Model):
1309
1310 name = models.CharField(max_length=32, unique = True)
1311 giturl = GitURLField()
1312 branch = models.CharField(max_length=32)
1313 dirpath = models.CharField(max_length=255)
1314
1315 def __unicode__(self):
1316 return "%s (Branch: %s)" % (self.name, self.branch)
1317
1318
1319class Release(models.Model):
1320 """ A release is a project template, used to pre-populate Project settings with a configuration set """
1321 name = models.CharField(max_length=32, unique = True)
1322 description = models.CharField(max_length=255)
1323 bitbake_version = models.ForeignKey(BitbakeVersion)
1324 branch_name = models.CharField(max_length=50, default = "")
1325 helptext = models.TextField(null=True)
1326
1327 def __unicode__(self):
1328 return "%s (%s)" % (self.name, self.branch_name)
1329
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001330 def __str__(self):
1331 return self.name
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001332
1333class ReleaseDefaultLayer(models.Model):
1334 release = models.ForeignKey(Release)
1335 layer_name = models.CharField(max_length=100, default="")
1336
1337
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001338class LayerSource(object):
1339 """ Where the layer metadata came from """
1340 TYPE_LOCAL = 0
1341 TYPE_LAYERINDEX = 1
1342 TYPE_IMPORTED = 2
1343 TYPE_BUILD = 3
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001344
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001345 SOURCE_TYPE = (
1346 (TYPE_LOCAL, "local"),
1347 (TYPE_LAYERINDEX, "layerindex"),
1348 (TYPE_IMPORTED, "imported"),
1349 (TYPE_BUILD, "build"),
1350 )
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001351
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001352 def types_dict():
1353 """ Turn the TYPES enums into a simple dictionary """
1354 dictionary = {}
1355 for key in LayerSource.__dict__:
1356 if "TYPE" in key:
1357 dictionary[key] = getattr(LayerSource, key)
1358 return dictionary
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001359
1360
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001361class Layer(models.Model):
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001362
1363 up_date = models.DateTimeField(null=True, default=timezone.now)
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001364
1365 name = models.CharField(max_length=100)
1366 layer_index_url = models.URLField()
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001367 vcs_url = GitURLField(default=None, null=True)
1368 local_source_dir = models.TextField(null = True, default = None)
1369 vcs_web_url = models.URLField(null=True, default=None)
1370 vcs_web_tree_base_url = models.URLField(null=True, default=None)
1371 vcs_web_file_base_url = models.URLField(null=True, default=None)
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001372
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001373 summary = models.TextField(help_text='One-line description of the layer',
1374 null=True, default=None)
1375 description = models.TextField(null=True, default=None)
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001376
1377 def __unicode__(self):
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001378 return "%s / %s " % (self.name, self.summary)
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001379
1380
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001381class Layer_Version(models.Model):
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001382 """
1383 A Layer_Version either belongs to a single project or no project
1384 """
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001385 search_allowed_fields = ["layer__name", "layer__summary",
1386 "layer__description", "layer__vcs_url",
1387 "dirpath", "release__name", "commit", "branch"]
1388
1389 build = models.ForeignKey(Build, related_name='layer_version_build',
1390 default=None, null=True)
1391
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001392 layer = models.ForeignKey(Layer, related_name='layer_version_layer')
1393
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001394 layer_source = models.IntegerField(choices=LayerSource.SOURCE_TYPE,
1395 default=0)
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001396
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001397 up_date = models.DateTimeField(null=True, default=timezone.now)
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001398
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001399 # To which metadata release does this layer version belong to
1400 release = models.ForeignKey(Release, null=True, default=None)
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001401
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001402 branch = models.CharField(max_length=80)
1403 commit = models.CharField(max_length=100)
1404 # If the layer is in a subdir
1405 dirpath = models.CharField(max_length=255, null=True, default=None)
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001406
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001407 # if -1, this is a default layer
1408 priority = models.IntegerField(default=0)
1409
1410 # where this layer exists on the filesystem
1411 local_path = models.FilePathField(max_length=1024, default="/")
1412
1413 # Set if this layer is restricted to a particular project
1414 project = models.ForeignKey('Project', null=True, default=None)
1415
1416 # code lifted, with adaptations, from the layerindex-web application
1417 # https://git.yoctoproject.org/cgit/cgit.cgi/layerindex-web/
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001418 def _handle_url_path(self, base_url, path):
1419 import re, posixpath
1420 if base_url:
1421 if self.dirpath:
1422 if path:
1423 extra_path = self.dirpath + '/' + path
1424 # Normalise out ../ in path for usage URL
1425 extra_path = posixpath.normpath(extra_path)
1426 # Minor workaround to handle case where subdirectory has been added between branches
1427 # (should probably support usage URL per branch to handle this... sigh...)
1428 if extra_path.startswith('../'):
1429 extra_path = extra_path[3:]
1430 else:
1431 extra_path = self.dirpath
1432 else:
1433 extra_path = path
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001434 branchname = self.release.name
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001435 url = base_url.replace('%branch%', branchname)
1436
1437 # If there's a % in the path (e.g. a wildcard bbappend) we need to encode it
1438 if extra_path:
1439 extra_path = extra_path.replace('%', '%25')
1440
1441 if '%path%' in base_url:
1442 if extra_path:
1443 url = re.sub(r'\[([^\]]*%path%[^\]]*)\]', '\\1', url)
1444 else:
1445 url = re.sub(r'\[([^\]]*%path%[^\]]*)\]', '', url)
1446 return url.replace('%path%', extra_path)
1447 else:
1448 return url + extra_path
1449 return None
1450
1451 def get_vcs_link_url(self):
1452 if self.layer.vcs_web_url is None:
1453 return None
1454 return self.layer.vcs_web_url
1455
1456 def get_vcs_file_link_url(self, file_path=""):
1457 if self.layer.vcs_web_file_base_url is None:
1458 return None
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001459 return self._handle_url_path(self.layer.vcs_web_file_base_url,
1460 file_path)
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001461
1462 def get_vcs_dirpath_link_url(self):
1463 if self.layer.vcs_web_tree_base_url is None:
1464 return None
1465 return self._handle_url_path(self.layer.vcs_web_tree_base_url, '')
1466
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001467 def get_vcs_reference(self):
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001468 if self.branch is not None and len(self.branch) > 0:
1469 return self.branch
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001470 if self.release is not None:
1471 return self.release.name
Patrick Williamsf1e5d692016-03-30 15:21:19 -05001472 if self.commit is not None and len(self.commit) > 0:
1473 return self.commit
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001474 return 'N/A'
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001475
1476 def get_detailspage_url(self, project_id):
1477 return reverse('layerdetails', args=(project_id, self.pk))
1478
Patrick Williamsf1e5d692016-03-30 15:21:19 -05001479 def get_alldeps(self, project_id):
1480 """Get full list of unique layer dependencies."""
1481 def gen_layerdeps(lver, project):
1482 for ldep in lver.dependencies.all():
1483 yield ldep.depends_on
1484 # get next level of deps recursively calling gen_layerdeps
1485 for subdep in gen_layerdeps(ldep.depends_on, project):
1486 yield subdep
1487
1488 project = Project.objects.get(pk=project_id)
1489 result = []
1490 projectlvers = [player.layercommit for player in project.projectlayer_set.all()]
1491 for dep in gen_layerdeps(self, project):
1492 # filter out duplicates and layers already belonging to the project
1493 if dep not in result + projectlvers:
1494 result.append(dep)
1495
1496 return sorted(result, key=lambda x: x.layer.name)
1497
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001498 def __unicode__(self):
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001499 return ("id %d belongs to layer: %s" % (self.pk, self.layer.name))
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001500
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001501 def __str__(self):
1502 if self.release:
1503 release = self.release.name
1504 else:
1505 release = "No release set"
1506
1507 return "%d %s (%s)" % (self.pk, self.layer.name, release)
1508
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001509
1510class LayerVersionDependency(models.Model):
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001511
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001512 layer_version = models.ForeignKey(Layer_Version,
1513 related_name="dependencies")
1514 depends_on = models.ForeignKey(Layer_Version,
1515 related_name="dependees")
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001516
1517class ProjectLayer(models.Model):
1518 project = models.ForeignKey(Project)
1519 layercommit = models.ForeignKey(Layer_Version, null=True)
1520 optional = models.BooleanField(default = True)
1521
1522 def __unicode__(self):
1523 return "%s, %s" % (self.project.name, self.layercommit)
1524
1525 class Meta:
1526 unique_together = (("project", "layercommit"),)
1527
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001528class CustomImageRecipe(Recipe):
Patrick Williamsf1e5d692016-03-30 15:21:19 -05001529
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001530 # CustomImageRecipe's belong to layers called:
1531 LAYER_NAME = "toaster-custom-images"
1532
1533 search_allowed_fields = ['name']
1534 base_recipe = models.ForeignKey(Recipe, related_name='based_on_recipe')
1535 project = models.ForeignKey(Project)
1536 last_updated = models.DateTimeField(null=True, default=None)
1537
1538 def get_last_successful_built_target(self):
1539 """ Return the last successful built target object if one exists
1540 otherwise return None """
1541 return Target.objects.filter(Q(build__outcome=Build.SUCCEEDED) &
1542 Q(build__project=self.project) &
1543 Q(target=self.name)).last()
1544
1545 def update_package_list(self):
1546 """ Update the package list from the last good build of this
1547 CustomImageRecipe
1548 """
1549 # Check if we're aldready up-to-date or not
1550 target = self.get_last_successful_built_target()
1551 if target == None:
1552 # So we've never actually built this Custom recipe but what about
1553 # the recipe it's based on?
1554 target = \
1555 Target.objects.filter(Q(build__outcome=Build.SUCCEEDED) &
1556 Q(build__project=self.project) &
1557 Q(target=self.base_recipe.name)).last()
1558 if target == None:
1559 return
1560
1561 if target.build.completed_on == self.last_updated:
1562 return
1563
1564 self.includes_set.clear()
1565
1566 excludes_list = self.excludes_set.values_list('name', flat=True)
1567 appends_list = self.appends_set.values_list('name', flat=True)
1568
1569 built_packages_list = \
1570 target.target_installed_package_set.values_list('package__name',
1571 flat=True)
1572 for built_package in built_packages_list:
1573 # Is the built package in the custom packages list?
1574 if built_package in excludes_list:
1575 continue
1576
1577 if built_package in appends_list:
1578 continue
1579
1580 cust_img_p = \
1581 CustomImagePackage.objects.get(name=built_package)
1582 self.includes_set.add(cust_img_p)
1583
1584
1585 self.last_updated = target.build.completed_on
1586 self.save()
1587
1588 def get_all_packages(self):
1589 """Get the included packages and any appended packages"""
1590 self.update_package_list()
1591
1592 return CustomImagePackage.objects.filter((Q(recipe_appends=self) |
1593 Q(recipe_includes=self)) &
1594 ~Q(recipe_excludes=self))
1595
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001596 def get_base_recipe_file(self):
1597 """Get the base recipe file path if it exists on the file system"""
1598 path_schema_one = "%s/%s" % (self.base_recipe.layer_version.dirpath,
1599 self.base_recipe.file_path)
1600
1601 path_schema_two = self.base_recipe.file_path
1602
1603 if os.path.exists(path_schema_one):
1604 return path_schema_one
1605
1606 # The path may now be the full path if the recipe has been built
1607 if os.path.exists(path_schema_two):
1608 return path_schema_two
1609
1610 return None
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001611
1612 def generate_recipe_file_contents(self):
1613 """Generate the contents for the recipe file."""
1614 # If we have no excluded packages we only need to _append
1615 if self.excludes_set.count() == 0:
1616 packages_conf = "IMAGE_INSTALL_append = \" "
1617
1618 for pkg in self.appends_set.all():
1619 packages_conf += pkg.name+' '
1620 else:
1621 packages_conf = "IMAGE_FEATURES =\"\"\nIMAGE_INSTALL = \""
1622 # We add all the known packages to be built by this recipe apart
1623 # from locale packages which are are controlled with IMAGE_LINGUAS.
1624 for pkg in self.get_all_packages().exclude(
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001625 name__icontains="locale"):
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001626 packages_conf += pkg.name+' '
1627
1628 packages_conf += "\""
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001629
1630 base_recipe_path = self.get_base_recipe_file()
1631 if base_recipe_path:
1632 base_recipe = open(base_recipe_path, 'r').read()
1633 else:
1634 raise IOError("Based on recipe file not found")
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001635
1636 # Add a special case for when the recipe we have based a custom image
1637 # recipe on requires another recipe.
1638 # For example:
1639 # "require core-image-minimal.bb" is changed to:
1640 # "require recipes-core/images/core-image-minimal.bb"
1641
1642 req_search = re.search(r'(require\s+)(.+\.bb\s*$)',
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001643 base_recipe,
1644 re.MULTILINE)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001645 if req_search:
1646 require_filename = req_search.group(2).strip()
1647
1648 corrected_location = Recipe.objects.filter(
1649 Q(layer_version=self.base_recipe.layer_version) &
1650 Q(file_path__icontains=require_filename)).last().file_path
1651
1652 new_require_line = "require %s" % corrected_location
1653
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001654 base_recipe = base_recipe.replace(req_search.group(0),
1655 new_require_line)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001656
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001657 info = {
1658 "date": timezone.now().strftime("%Y-%m-%d %H:%M:%S"),
1659 "base_recipe": base_recipe,
1660 "recipe_name": self.name,
1661 "base_recipe_name": self.base_recipe.name,
1662 "license": self.license,
1663 "summary": self.summary,
1664 "description": self.description,
1665 "packages_conf": packages_conf.strip()
1666 }
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001667
1668 recipe_contents = ("# Original recipe %(base_recipe_name)s \n"
1669 "%(base_recipe)s\n\n"
1670 "# Recipe %(recipe_name)s \n"
1671 "# Customisation Generated by Toaster on %(date)s\n"
1672 "SUMMARY = \"%(summary)s\"\n"
1673 "DESCRIPTION = \"%(description)s\"\n"
1674 "LICENSE = \"%(license)s\"\n"
1675 "%(packages_conf)s") % info
1676
1677 return recipe_contents
Patrick Williamsf1e5d692016-03-30 15:21:19 -05001678
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001679class ProjectVariable(models.Model):
1680 project = models.ForeignKey(Project)
1681 name = models.CharField(max_length=100)
1682 value = models.TextField(blank = True)
1683
1684class Variable(models.Model):
1685 search_allowed_fields = ['variable_name', 'variable_value',
1686 'vhistory__file_name', "description"]
1687 build = models.ForeignKey(Build, related_name='variable_build')
1688 variable_name = models.CharField(max_length=100)
1689 variable_value = models.TextField(blank=True)
1690 changed = models.BooleanField(default=False)
1691 human_readable_name = models.CharField(max_length=200)
1692 description = models.TextField(blank=True)
1693
1694class VariableHistory(models.Model):
1695 variable = models.ForeignKey(Variable, related_name='vhistory')
1696 value = models.TextField(blank=True)
1697 file_name = models.FilePathField(max_length=255)
1698 line_number = models.IntegerField(null=True)
1699 operation = models.CharField(max_length=64)
1700
1701class HelpText(models.Model):
1702 VARIABLE = 0
1703 HELPTEXT_AREA = ((VARIABLE, 'variable'), )
1704
1705 build = models.ForeignKey(Build, related_name='helptext_build')
1706 area = models.IntegerField(choices=HELPTEXT_AREA)
1707 key = models.CharField(max_length=100)
1708 text = models.TextField()
1709
1710class LogMessage(models.Model):
1711 EXCEPTION = -1 # used to signal self-toaster-exceptions
1712 INFO = 0
1713 WARNING = 1
1714 ERROR = 2
Patrick Williamsf1e5d692016-03-30 15:21:19 -05001715 CRITICAL = 3
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001716
Patrick Williamsf1e5d692016-03-30 15:21:19 -05001717 LOG_LEVEL = (
1718 (INFO, "info"),
1719 (WARNING, "warn"),
1720 (ERROR, "error"),
1721 (CRITICAL, "critical"),
1722 (EXCEPTION, "toaster exception")
1723 )
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001724
1725 build = models.ForeignKey(Build)
1726 task = models.ForeignKey(Task, blank = True, null=True)
1727 level = models.IntegerField(choices=LOG_LEVEL, default=INFO)
Patrick Williamsf1e5d692016-03-30 15:21:19 -05001728 message = models.TextField(blank=True, null=True)
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001729 pathname = models.FilePathField(max_length=255, blank=True)
1730 lineno = models.IntegerField(null=True)
1731
1732 def __str__(self):
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001733 return force_bytes('%s %s %s' % (self.get_level_display(), self.message, self.build))
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001734
1735def invalidate_cache(**kwargs):
1736 from django.core.cache import cache
1737 try:
1738 cache.clear()
1739 except Exception as e:
1740 logger.warning("Problem with cache backend: Failed to clear cache: %s" % e)
1741
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001742def signal_runbuilds():
1743 """Send SIGUSR1 to runbuilds process"""
1744 with open(os.path.join(os.getenv('BUILDDIR'), '.runbuilds.pid')) as pidf:
1745 os.kill(int(pidf.read()), SIGUSR1)
1746
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001747django.db.models.signals.post_save.connect(invalidate_cache)
1748django.db.models.signals.post_delete.connect(invalidate_cache)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001749django.db.models.signals.m2m_changed.connect(invalidate_cache)