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