1#
2# BitBake ToasterUI Implementation
3#
4# Copyright (C) 2013        Intel Corporation
5#
6# SPDX-License-Identifier: GPL-2.0-only
7#
8
9import sys
10import bb
11import re
12import os
13
14import django
15from django.utils import timezone
16
17import toaster
18# Add toaster module to the search path to help django.setup() find the right
19# modules
20sys.path.insert(0, os.path.dirname(toaster.__file__))
21
22#Set the DJANGO_SETTINGS_MODULE if it's not already set
23os.environ["DJANGO_SETTINGS_MODULE"] =\
24    os.environ.get("DJANGO_SETTINGS_MODULE",
25                   "toaster.toastermain.settings")
26# Setup django framework (needs to be done before importing modules)
27django.setup()
28
29from orm.models import Build, Task, Recipe, Layer_Version, Layer, Target, LogMessage, HelpText
30from orm.models import Target_Image_File, TargetKernelFile, TargetSDKFile
31from orm.models import Variable, VariableHistory
32from orm.models import Package, Package_File, Target_Installed_Package, Target_File
33from orm.models import Task_Dependency, Package_Dependency
34from orm.models import Recipe_Dependency, Provides
35from orm.models import Project, CustomImagePackage
36from orm.models import signal_runbuilds
37
38from bldcontrol.models import BuildEnvironment, BuildRequest
39from bldcontrol.models import BRLayer
40from bldcontrol import bbcontroller
41
42from bb.msg import BBLogFormatter as formatter
43from django.db import models
44from pprint import pformat
45import logging
46from datetime import datetime, timedelta
47
48from django.db import transaction, connection
49
50
51# pylint: disable=invalid-name
52# the logger name is standard throughout BitBake
53logger = logging.getLogger("ToasterLogger")
54
55class NotExisting(Exception):
56    pass
57
58class ORMWrapper(object):
59    """ This class creates the dictionaries needed to store information in the database
60        following the format defined by the Django models. It is also used to save this
61        information in the database.
62    """
63
64    def __init__(self):
65        self.layer_version_objects = []
66        self.layer_version_built = []
67        self.task_objects = {}
68        self.recipe_objects = {}
69
70    @staticmethod
71    def _build_key(**kwargs):
72        key = "0"
73        for k in sorted(kwargs.keys()):
74            if isinstance(kwargs[k], models.Model):
75                key += "-%d" % kwargs[k].id
76            else:
77                key += "-%s" % str(kwargs[k])
78        return key
79
80
81    def _cached_get_or_create(self, clazz, **kwargs):
82        """ This is a memory-cached get_or_create. We assume that the objects will not be created in the
83            database through any other means.
84        """
85
86        assert issubclass(clazz, models.Model), "_cached_get_or_create needs to get the class as first argument"
87
88        key = ORMWrapper._build_key(**kwargs)
89        dictname = "objects_%s" % clazz.__name__
90        if not dictname in vars(self).keys():
91            vars(self)[dictname] = {}
92
93        created = False
94        if not key in vars(self)[dictname].keys():
95            vars(self)[dictname][key], created = \
96                clazz.objects.get_or_create(**kwargs)
97
98        return (vars(self)[dictname][key], created)
99
100
101    def _cached_get(self, clazz, **kwargs):
102        """ This is a memory-cached get. We assume that the objects will not change  in the database between gets.
103        """
104        assert issubclass(clazz, models.Model), "_cached_get needs to get the class as first argument"
105
106        key = ORMWrapper._build_key(**kwargs)
107        dictname = "objects_%s" % clazz.__name__
108
109        if not dictname in vars(self).keys():
110            vars(self)[dictname] = {}
111
112        if not key in vars(self)[dictname].keys():
113            vars(self)[dictname][key] = clazz.objects.get(**kwargs)
114
115        return vars(self)[dictname][key]
116
117    def get_similar_target_with_image_files(self, target):
118        """
119        Get a Target object "similar" to target; i.e. with the same target
120        name ('core-image-minimal' etc.) and machine.
121        """
122        return target.get_similar_target_with_image_files()
123
124    def get_similar_target_with_sdk_files(self, target):
125        return target.get_similar_target_with_sdk_files()
126
127    def clone_image_artifacts(self, target_from, target_to):
128        target_to.clone_image_artifacts_from(target_from)
129
130    def clone_sdk_artifacts(self, target_from, target_to):
131        target_to.clone_sdk_artifacts_from(target_from)
132
133    def _timestamp_to_datetime(self, secs):
134        """
135        Convert timestamp in seconds to Python datetime
136        """
137        return timezone.make_aware(datetime(1970, 1, 1) + timedelta(seconds=secs))
138
139    # pylint: disable=no-self-use
140    # we disable detection of no self use in functions because the methods actually work on the object
141    # even if they don't touch self anywhere
142
143    # pylint: disable=bad-continuation
144    # we do not follow the python conventions for continuation indentation due to long lines here
145
146    def get_or_create_build_object(self, brbe):
147        prj = None
148        buildrequest = None
149        if brbe is not None:
150            # Toaster-triggered build
151            logger.debug("buildinfohelper: brbe is %s" % brbe)
152            br, _ = brbe.split(":")
153            buildrequest = BuildRequest.objects.get(pk=br)
154            prj = buildrequest.project
155        else:
156            # CLI build
157            prj = Project.objects.get_or_create_default_project()
158            logger.debug("buildinfohelper: project is not specified, defaulting to %s" % prj)
159
160        if buildrequest is not None:
161            # reuse existing Build object
162            build = buildrequest.build
163            build.project = prj
164            build.save()
165        else:
166            # create new Build object
167            now = timezone.now()
168            build = Build.objects.create(
169                project=prj,
170                started_on=now,
171                completed_on=now,
172                build_name='')
173
174        logger.debug("buildinfohelper: build is created %s" % build)
175
176        if buildrequest is not None:
177            buildrequest.build = build
178            buildrequest.save()
179
180        return build
181
182    def update_build(self, build, data_dict):
183        for key in data_dict:
184            setattr(build, key, data_dict[key])
185        build.save()
186
187    @staticmethod
188    def get_or_create_targets(target_info):
189        """
190        NB get_or_create() is used here because for Toaster-triggered builds,
191        we already created the targets when the build was triggered.
192        """
193        result = []
194        for target in target_info['targets']:
195            task = ''
196            if ':' in target:
197                target, task = target.split(':', 1)
198            if task.startswith('do_'):
199                task = task[3:]
200            if task == 'build':
201                task = ''
202
203            obj, _ = Target.objects.get_or_create(build=target_info['build'],
204                                                  target=target,
205                                                  task=task)
206            result.append(obj)
207        return result
208
209    def update_build_stats_and_outcome(self, build, errors, warnings, taskfailures):
210        assert isinstance(build,Build)
211        assert isinstance(errors, int)
212        assert isinstance(warnings, int)
213
214        if build.outcome == Build.CANCELLED:
215            return
216        try:
217            if build.buildrequest.state == BuildRequest.REQ_CANCELLING:
218                return
219        except AttributeError:
220            # We may not have a buildrequest if this is a command line build
221            pass
222
223        outcome = Build.SUCCEEDED
224        if errors or taskfailures:
225            outcome = Build.FAILED
226
227        build.completed_on = timezone.now()
228        build.outcome = outcome
229        build.save()
230        signal_runbuilds()
231
232    def update_target_set_license_manifest(self, target, license_manifest_path):
233        target.license_manifest_path = license_manifest_path
234        target.save()
235
236    def update_target_set_package_manifest(self, target, package_manifest_path):
237        target.package_manifest_path = package_manifest_path
238        target.save()
239
240    def update_task_object(self, build, task_name, recipe_name, task_stats):
241        """
242        Find the task for build which matches the recipe and task name
243        to be stored
244        """
245        task_to_update = Task.objects.get(
246            build = build,
247            task_name = task_name,
248            recipe__name = recipe_name
249        )
250
251        if 'started' in task_stats and 'ended' in task_stats:
252            task_to_update.started = self._timestamp_to_datetime(task_stats['started'])
253            task_to_update.ended = self._timestamp_to_datetime(task_stats['ended'])
254            task_to_update.elapsed_time = (task_stats['ended'] - task_stats['started'])
255        task_to_update.cpu_time_user = task_stats.get('cpu_time_user')
256        task_to_update.cpu_time_system = task_stats.get('cpu_time_system')
257        if 'disk_io_read' in task_stats and 'disk_io_write' in task_stats:
258            task_to_update.disk_io_read = task_stats['disk_io_read']
259            task_to_update.disk_io_write = task_stats['disk_io_write']
260            task_to_update.disk_io = task_stats['disk_io_read'] + task_stats['disk_io_write']
261
262        task_to_update.save()
263
264    def get_update_task_object(self, task_information, must_exist = False):
265        assert 'build' in task_information
266        assert 'recipe' in task_information
267        assert 'task_name' in task_information
268
269        # we use must_exist info for database look-up optimization
270        task_object, created = self._cached_get_or_create(Task,
271                        build=task_information['build'],
272                        recipe=task_information['recipe'],
273                        task_name=task_information['task_name']
274                        )
275        if created and must_exist:
276            task_information['debug'] = "build id %d, recipe id %d" % (task_information['build'].pk, task_information['recipe'].pk)
277            raise NotExisting("Task object created when expected to exist", task_information)
278
279        object_changed = False
280        for v in vars(task_object):
281            if v in task_information.keys():
282                if vars(task_object)[v] != task_information[v]:
283                    vars(task_object)[v] = task_information[v]
284                    object_changed = True
285
286        # update setscene-related information if the task has a setscene
287        if task_object.outcome == Task.OUTCOME_COVERED and 1 == task_object.get_related_setscene().count():
288            task_object.outcome = Task.OUTCOME_CACHED
289            object_changed = True
290
291            outcome_task_setscene = Task.objects.get(task_executed=True, build = task_object.build,
292                                    recipe = task_object.recipe, task_name=task_object.task_name+"_setscene").outcome
293            if outcome_task_setscene == Task.OUTCOME_SUCCESS:
294                task_object.sstate_result = Task.SSTATE_RESTORED
295                object_changed = True
296            elif outcome_task_setscene == Task.OUTCOME_FAILED:
297                task_object.sstate_result = Task.SSTATE_FAILED
298                object_changed = True
299
300        if object_changed:
301            task_object.save()
302        return task_object
303
304
305    def get_update_recipe_object(self, recipe_information, must_exist = False):
306        assert 'layer_version' in recipe_information
307        assert 'file_path' in recipe_information
308        assert 'pathflags' in recipe_information
309
310        assert not recipe_information['file_path'].startswith("/")      # we should have layer-relative paths at all times
311
312
313        def update_recipe_obj(recipe_object):
314            object_changed = False
315            for v in vars(recipe_object):
316                if v in recipe_information.keys():
317                    object_changed = True
318                    vars(recipe_object)[v] = recipe_information[v]
319
320            if object_changed:
321                recipe_object.save()
322
323        recipe, created = self._cached_get_or_create(Recipe, layer_version=recipe_information['layer_version'],
324                                     file_path=recipe_information['file_path'], pathflags = recipe_information['pathflags'])
325
326        update_recipe_obj(recipe)
327
328        built_recipe = None
329        # Create a copy of the recipe for historical puposes and update it
330        for built_layer in self.layer_version_built:
331            if built_layer.layer == recipe_information['layer_version'].layer:
332                built_recipe, c = self._cached_get_or_create(Recipe,
333                        layer_version=built_layer,
334                        file_path=recipe_information['file_path'],
335                        pathflags = recipe_information['pathflags'])
336                update_recipe_obj(built_recipe)
337                break
338
339
340        # If we're in analysis mode or if this is a custom recipe
341        # then we are wholly responsible for the data
342        # and therefore we return the 'real' recipe rather than the build
343        # history copy of the recipe.
344        if  recipe_information['layer_version'].build is not None and \
345            recipe_information['layer_version'].build.project == \
346                Project.objects.get_or_create_default_project():
347            return recipe
348
349        if built_recipe is None:
350            return recipe
351
352        return built_recipe
353
354    def get_update_layer_version_object(self, build_obj, layer_obj, layer_version_information):
355        if isinstance(layer_obj, Layer_Version):
356            # We already found our layer version for this build so just
357            # update it with the new build information
358            logger.debug("We found our layer from toaster")
359            layer_obj.local_path = layer_version_information['local_path']
360            layer_obj.save()
361            self.layer_version_objects.append(layer_obj)
362
363            # create a new copy of this layer version as a snapshot for
364            # historical purposes
365            layer_copy, c = Layer_Version.objects.get_or_create(
366                build=build_obj,
367                layer=layer_obj.layer,
368                release=layer_obj.release,
369                branch=layer_version_information['branch'],
370                commit=layer_version_information['commit'],
371                local_path=layer_version_information['local_path'],
372            )
373
374            logger.debug("Created new layer version %s for build history",
375                         layer_copy.layer.name)
376
377            self.layer_version_built.append(layer_copy)
378
379            return layer_obj
380
381        assert isinstance(build_obj, Build)
382        assert isinstance(layer_obj, Layer)
383        assert 'branch' in layer_version_information
384        assert 'commit' in layer_version_information
385        assert 'priority' in layer_version_information
386        assert 'local_path' in layer_version_information
387
388        # If we're doing a command line build then associate this new layer with the
389        # project to avoid it 'contaminating' toaster data
390        project = None
391        if build_obj.project == Project.objects.get_or_create_default_project():
392            project = build_obj.project
393
394        layer_version_object, _ = Layer_Version.objects.get_or_create(
395                                  build = build_obj,
396                                  layer = layer_obj,
397                                  branch = layer_version_information['branch'],
398                                  commit = layer_version_information['commit'],
399                                  priority = layer_version_information['priority'],
400                                  local_path = layer_version_information['local_path'],
401                                  project=project)
402
403        self.layer_version_objects.append(layer_version_object)
404
405        return layer_version_object
406
407    def get_update_layer_object(self, layer_information, brbe):
408        assert 'name' in layer_information
409        assert 'layer_index_url' in layer_information
410
411        # From command line builds we have no brbe as the request is directly
412        # from bitbake
413        if brbe is None:
414            # If we don't have git commit sha then we're using a non-git
415            # layer so set the layer_source_dir to identify it as such
416            if not layer_information['version']['commit']:
417                local_source_dir = layer_information["local_path"]
418            else:
419                local_source_dir = None
420
421            layer_object, _ = \
422                Layer.objects.get_or_create(
423                    name=layer_information['name'],
424                    local_source_dir=local_source_dir,
425                    layer_index_url=layer_information['layer_index_url'])
426
427            return layer_object
428        else:
429            br_id, be_id = brbe.split(":")
430
431            # Find the layer version by matching the layer event information
432            # against the metadata we have in Toaster
433
434            try:
435                br_layer = BRLayer.objects.get(req=br_id,
436                                               name=layer_information['name'])
437                return br_layer.layer_version
438            except (BRLayer.MultipleObjectsReturned, BRLayer.DoesNotExist):
439                # There are multiple of the same layer name or the name
440                # hasn't been determined by the toaster.bbclass layer
441                # so let's filter by the local_path
442                bc = bbcontroller.getBuildEnvironmentController(pk=be_id)
443                for br_layer in BRLayer.objects.filter(req=br_id):
444                    if br_layer.giturl and \
445                       layer_information['local_path'].endswith(
446                           bc.getGitCloneDirectory(br_layer.giturl,
447                                                   br_layer.commit)):
448                            return br_layer.layer_version
449
450                    if br_layer.local_source_dir == \
451                            layer_information['local_path']:
452                        return br_layer.layer_version
453
454        # We've reached the end of our search and couldn't find the layer
455        # we can continue but some data may be missing
456        raise NotExisting("Unidentified layer %s" %
457                          pformat(layer_information))
458
459    def save_target_file_information(self, build_obj, target_obj, filedata):
460        assert isinstance(build_obj, Build)
461        assert isinstance(target_obj, Target)
462        dirs = filedata['dirs']
463        files = filedata['files']
464        syms = filedata['syms']
465
466        # always create the root directory as a special case;
467        # note that this is never displayed, so the owner, group,
468        # size, permission are irrelevant
469        tf_obj = Target_File.objects.create(target = target_obj,
470                                            path = '/',
471                                            size = 0,
472                                            owner = '',
473                                            group = '',
474                                            permission = '',
475                                            inodetype = Target_File.ITYPE_DIRECTORY)
476        tf_obj.save()
477
478        # insert directories, ordered by name depth
479        for d in sorted(dirs, key=lambda x:len(x[-1].split("/"))):
480            (user, group, size) = d[1:4]
481            permission = d[0][1:]
482            path = d[4].lstrip(".")
483
484            # we already created the root directory, so ignore any
485            # entry for it
486            if len(path) == 0:
487                continue
488
489            parent_path = "/".join(path.split("/")[:len(path.split("/")) - 1])
490            if len(parent_path) == 0:
491                parent_path = "/"
492            parent_obj = self._cached_get(Target_File, target = target_obj, path = parent_path, inodetype = Target_File.ITYPE_DIRECTORY)
493            tf_obj = Target_File.objects.create(
494                        target = target_obj,
495                        path = path,
496                        size = size,
497                        inodetype = Target_File.ITYPE_DIRECTORY,
498                        permission = permission,
499                        owner = user,
500                        group = group,
501                        directory = parent_obj)
502
503
504        # we insert files
505        for d in files:
506            (user, group, size) = d[1:4]
507            permission = d[0][1:]
508            path = d[4].lstrip(".")
509            parent_path = "/".join(path.split("/")[:len(path.split("/")) - 1])
510            inodetype = Target_File.ITYPE_REGULAR
511            if d[0].startswith('b'):
512                inodetype = Target_File.ITYPE_BLOCK
513            if d[0].startswith('c'):
514                inodetype = Target_File.ITYPE_CHARACTER
515            if d[0].startswith('p'):
516                inodetype = Target_File.ITYPE_FIFO
517
518            tf_obj = Target_File.objects.create(
519                        target = target_obj,
520                        path = path,
521                        size = size,
522                        inodetype = inodetype,
523                        permission = permission,
524                        owner = user,
525                        group = group)
526            parent_obj = self._cached_get(Target_File, target = target_obj, path = parent_path, inodetype = Target_File.ITYPE_DIRECTORY)
527            tf_obj.directory = parent_obj
528            tf_obj.save()
529
530        # we insert symlinks
531        for d in syms:
532            (user, group, size) = d[1:4]
533            permission = d[0][1:]
534            path = d[4].lstrip(".")
535            filetarget_path = d[6]
536
537            parent_path = "/".join(path.split("/")[:len(path.split("/")) - 1])
538            if not filetarget_path.startswith("/"):
539                # we have a relative path, get a normalized absolute one
540                filetarget_path = parent_path + "/" + filetarget_path
541                fcp = filetarget_path.split("/")
542                fcpl = []
543                for i in fcp:
544                    if i == "..":
545                        fcpl.pop()
546                    else:
547                        fcpl.append(i)
548                filetarget_path = "/".join(fcpl)
549
550            try:
551                filetarget_obj = Target_File.objects.get(target = target_obj, path = filetarget_path)
552            except Target_File.DoesNotExist:
553                # we might have an invalid link; no way to detect this. just set it to None
554                filetarget_obj = None
555
556            parent_obj = Target_File.objects.get(target = target_obj, path = parent_path, inodetype = Target_File.ITYPE_DIRECTORY)
557
558            tf_obj = Target_File.objects.create(
559                        target = target_obj,
560                        path = path,
561                        size = size,
562                        inodetype = Target_File.ITYPE_SYMLINK,
563                        permission = permission,
564                        owner = user,
565                        group = group,
566                        directory = parent_obj,
567                        sym_target = filetarget_obj)
568
569
570    def save_target_package_information(self, build_obj, target_obj, packagedict, pkgpnmap, recipes, built_package=False):
571        assert isinstance(build_obj, Build)
572        assert isinstance(target_obj, Target)
573
574        errormsg = ""
575        for p in packagedict:
576            # Search name swtiches round the installed name vs package name
577            # by default installed name == package name
578            searchname = p
579            if p not in pkgpnmap:
580                logger.warning("Image packages list contains %p, but is"
581                               " missing from all packages list where the"
582                               " metadata comes from. Skipping...", p)
583                continue
584
585            if 'OPKGN' in pkgpnmap[p].keys():
586                searchname = pkgpnmap[p]['OPKGN']
587
588            built_recipe = recipes[pkgpnmap[p]['PN']]
589
590            if built_package:
591                packagedict[p]['object'], created = Package.objects.get_or_create( build = build_obj, name = searchname )
592                recipe = built_recipe
593            else:
594                packagedict[p]['object'], created = \
595                        CustomImagePackage.objects.get_or_create(name=searchname)
596                # Clear the Package_Dependency objects as we're going to update
597                # the CustomImagePackage with the latest dependency information
598                packagedict[p]['object'].package_dependencies_target.all().delete()
599                packagedict[p]['object'].package_dependencies_source.all().delete()
600                try:
601                    recipe = self._cached_get(
602                        Recipe,
603                        name=built_recipe.name,
604                        layer_version__build=None,
605                        layer_version__release=
606                        built_recipe.layer_version.release,
607                        file_path=built_recipe.file_path,
608                        version=built_recipe.version
609                    )
610                except (Recipe.DoesNotExist,
611                        Recipe.MultipleObjectsReturned) as e:
612                    logger.info("We did not find one recipe for the"
613                                "configuration data package %s %s" % (p, e))
614                    continue
615
616            if created or packagedict[p]['object'].size == -1:    # save the data anyway we can, not just if it was not created here; bug [YOCTO #6887]
617                # fill in everything we can from the runtime-reverse package data
618                try:
619                    packagedict[p]['object'].recipe = recipe
620                    packagedict[p]['object'].version = pkgpnmap[p]['PV']
621                    packagedict[p]['object'].installed_name = p
622                    packagedict[p]['object'].revision = pkgpnmap[p]['PR']
623                    packagedict[p]['object'].license = pkgpnmap[p]['LICENSE']
624                    packagedict[p]['object'].section = pkgpnmap[p]['SECTION']
625                    packagedict[p]['object'].summary = pkgpnmap[p]['SUMMARY']
626                    packagedict[p]['object'].description = pkgpnmap[p]['DESCRIPTION']
627                    packagedict[p]['object'].size = int(pkgpnmap[p]['PKGSIZE'])
628
629                # no files recorded for this package, so save files info
630                    packagefile_objects = []
631                    for targetpath in pkgpnmap[p]['FILES_INFO']:
632                        targetfilesize = pkgpnmap[p]['FILES_INFO'][targetpath]
633                        packagefile_objects.append(Package_File( package = packagedict[p]['object'],
634                            path = targetpath,
635                            size = targetfilesize))
636                    if len(packagefile_objects):
637                        Package_File.objects.bulk_create(packagefile_objects)
638                except KeyError as e:
639                    errormsg += "  stpi: Key error, package %s key %s \n" % ( p, e )
640
641            # save disk installed size
642            packagedict[p]['object'].installed_size = packagedict[p]['size']
643            packagedict[p]['object'].save()
644
645            if built_package:
646                Target_Installed_Package.objects.create(target = target_obj, package = packagedict[p]['object'])
647
648        packagedeps_objs = []
649        pattern_so = re.compile(r'.*\.so(\.\d*)?$')
650        pattern_lib = re.compile(r'.*\-suffix(\d*)?$')
651        pattern_ko = re.compile(r'^kernel-module-.*')
652        for p in packagedict:
653            for (px,deptype) in packagedict[p]['depends']:
654                if deptype == 'depends':
655                    tdeptype = Package_Dependency.TYPE_TRDEPENDS
656                elif deptype == 'recommends':
657                    tdeptype = Package_Dependency.TYPE_TRECOMMENDS
658
659                try:
660                    # Skip known non-package objects like libraries and kernel modules
661                    if pattern_so.match(px) or pattern_lib.match(px):
662                        logger.info("Toaster does not add library file dependencies to packages (%s,%s)", p, px)
663                        continue
664                    if pattern_ko.match(px):
665                        logger.info("Toaster does not add kernel module dependencies to packages (%s,%s)", p, px)
666                        continue
667                    packagedeps_objs.append(Package_Dependency(
668                        package = packagedict[p]['object'],
669                        depends_on = packagedict[px]['object'],
670                        dep_type = tdeptype,
671                        target = target_obj))
672                except KeyError as e:
673                    logger.warning("Could not add dependency to the package %s "
674                                   "because %s is an unknown package", p, px)
675
676        if len(packagedeps_objs) > 0:
677            Package_Dependency.objects.bulk_create(packagedeps_objs)
678        else:
679            logger.info("No package dependencies created")
680
681        if len(errormsg) > 0:
682            logger.warning("buildinfohelper: target_package_info could not identify recipes: \n%s", errormsg)
683
684    def save_target_image_file_information(self, target_obj, file_name, file_size):
685        Target_Image_File.objects.create(target=target_obj,
686            file_name=file_name, file_size=file_size)
687
688    def save_target_kernel_file(self, target_obj, file_name, file_size):
689        """
690        Save kernel file (bzImage, modules*) information for a Target target_obj.
691        """
692        TargetKernelFile.objects.create(target=target_obj,
693            file_name=file_name, file_size=file_size)
694
695    def save_target_sdk_file(self, target_obj, file_name, file_size):
696        """
697        Save SDK artifacts to the database, associating them with a
698        Target object.
699        """
700        TargetSDKFile.objects.create(target=target_obj, file_name=file_name,
701            file_size=file_size)
702
703    def create_logmessage(self, log_information):
704        assert 'build' in log_information
705        assert 'level' in log_information
706        assert 'message' in log_information
707
708        log_object = LogMessage.objects.create(
709                        build = log_information['build'],
710                        level = log_information['level'],
711                        message = log_information['message'])
712
713        for v in vars(log_object):
714            if v in log_information.keys():
715                vars(log_object)[v] = log_information[v]
716
717        return log_object.save()
718
719
720    def save_build_package_information(self, build_obj, package_info, recipes,
721                                       built_package):
722        # assert isinstance(build_obj, Build)
723
724        if not 'PN' in package_info.keys():
725            # no package data to save (e.g. 'OPKGN'="lib64-*"|"lib32-*")
726            return None
727
728        # create and save the object
729        pname = package_info['PKG']
730        built_recipe = recipes[package_info['PN']]
731        if 'OPKGN' in package_info.keys():
732            pname = package_info['OPKGN']
733
734        if built_package:
735            bp_object, _ = Package.objects.get_or_create( build = build_obj,
736                                                         name = pname )
737            recipe = built_recipe
738        else:
739            bp_object, created = \
740                    CustomImagePackage.objects.get_or_create(name=pname)
741            try:
742                recipe = self._cached_get(Recipe,
743                                          name=built_recipe.name,
744                                          layer_version__build=None,
745                                          file_path=built_recipe.file_path,
746                                          version=built_recipe.version)
747
748            except (Recipe.DoesNotExist, Recipe.MultipleObjectsReturned):
749                logger.debug("We did not find one recipe for the configuration"
750                             "data package %s" % pname)
751                return
752
753        bp_object.installed_name = package_info['PKG']
754        bp_object.recipe = recipe
755        bp_object.version = package_info['PKGV']
756        bp_object.revision = package_info['PKGR']
757        bp_object.summary = package_info['SUMMARY']
758        bp_object.description = package_info['DESCRIPTION']
759        bp_object.size = int(package_info['PKGSIZE'])
760        bp_object.section = package_info['SECTION']
761        bp_object.license = package_info['LICENSE']
762        bp_object.save()
763
764        # save any attached file information
765        packagefile_objects = []
766        for path in package_info['FILES_INFO']:
767            packagefile_objects.append(Package_File( package = bp_object,
768                                        path = path,
769                                        size = package_info['FILES_INFO'][path] ))
770        if len(packagefile_objects):
771            Package_File.objects.bulk_create(packagefile_objects)
772
773        def _po_byname(p):
774            if built_package:
775                pkg, created = Package.objects.get_or_create(build=build_obj,
776                                                             name=p)
777            else:
778                pkg, created = CustomImagePackage.objects.get_or_create(name=p)
779
780            if created:
781                pkg.size = -1
782                pkg.save()
783            return pkg
784
785        packagedeps_objs = []
786        # save soft dependency information
787        if 'RDEPENDS' in package_info and package_info['RDEPENDS']:
788            for p in bb.utils.explode_deps(package_info['RDEPENDS']):
789                packagedeps_objs.append(Package_Dependency(  package = bp_object,
790                    depends_on = _po_byname(p), dep_type = Package_Dependency.TYPE_RDEPENDS))
791        if 'RPROVIDES' in package_info and package_info['RPROVIDES']:
792            for p in bb.utils.explode_deps(package_info['RPROVIDES']):
793                packagedeps_objs.append(Package_Dependency(  package = bp_object,
794                    depends_on = _po_byname(p), dep_type = Package_Dependency.TYPE_RPROVIDES))
795        if 'RRECOMMENDS' in package_info and package_info['RRECOMMENDS']:
796            for p in bb.utils.explode_deps(package_info['RRECOMMENDS']):
797                packagedeps_objs.append(Package_Dependency(  package = bp_object,
798                    depends_on = _po_byname(p), dep_type = Package_Dependency.TYPE_RRECOMMENDS))
799        if 'RSUGGESTS' in package_info and package_info['RSUGGESTS']:
800            for p in bb.utils.explode_deps(package_info['RSUGGESTS']):
801                packagedeps_objs.append(Package_Dependency(  package = bp_object,
802                    depends_on = _po_byname(p), dep_type = Package_Dependency.TYPE_RSUGGESTS))
803        if 'RREPLACES' in package_info and package_info['RREPLACES']:
804            for p in bb.utils.explode_deps(package_info['RREPLACES']):
805                packagedeps_objs.append(Package_Dependency(  package = bp_object,
806                    depends_on = _po_byname(p), dep_type = Package_Dependency.TYPE_RREPLACES))
807        if 'RCONFLICTS' in package_info and package_info['RCONFLICTS']:
808            for p in bb.utils.explode_deps(package_info['RCONFLICTS']):
809                packagedeps_objs.append(Package_Dependency(  package = bp_object,
810                    depends_on = _po_byname(p), dep_type = Package_Dependency.TYPE_RCONFLICTS))
811
812        if len(packagedeps_objs) > 0:
813            Package_Dependency.objects.bulk_create(packagedeps_objs)
814
815        return bp_object
816
817    def save_build_variables(self, build_obj, vardump):
818        assert isinstance(build_obj, Build)
819
820        for k in vardump:
821            desc = vardump[k]['doc']
822            if desc is None:
823                var_words = [word for word in k.split('_')]
824                root_var = "_".join([word for word in var_words if word.isupper()])
825                if root_var and root_var != k and root_var in vardump:
826                    desc = vardump[root_var]['doc']
827            if desc is None:
828                desc = ''
829            if len(desc):
830                HelpText.objects.get_or_create(build=build_obj,
831                                               area=HelpText.VARIABLE,
832                                               key=k, text=desc)
833            if not bool(vardump[k]['func']):
834                value = vardump[k]['v']
835                if value is None:
836                    value = ''
837                variable_obj = Variable.objects.create( build = build_obj,
838                    variable_name = k,
839                    variable_value = value,
840                    description = desc)
841
842                varhist_objects = []
843                for vh in vardump[k]['history']:
844                    if not 'documentation.conf' in vh['file']:
845                        varhist_objects.append(VariableHistory( variable = variable_obj,
846                                file_name = vh['file'],
847                                line_number = vh['line'],
848                                operation = vh['op']))
849                if len(varhist_objects):
850                    VariableHistory.objects.bulk_create(varhist_objects)
851
852
853class MockEvent(object):
854    """ This object is used to create event, for which normal event-processing methods can
855        be used, out of data that is not coming via an actual event
856    """
857    def __init__(self):
858        self.msg = None
859        self.levelno = None
860        self.taskname = None
861        self.taskhash = None
862        self.pathname = None
863        self.lineno = None
864
865    def getMessage(self):
866        """
867        Simulate LogRecord message return
868        """
869        return self.msg
870
871
872class BuildInfoHelper(object):
873    """ This class gathers the build information from the server and sends it
874        towards the ORM wrapper for storing in the database
875        It is instantiated once per build
876        Keeps in memory all data that needs matching before writing it to the database
877    """
878
879    # tasks which produce image files; note we include '', as we set
880    # the task for a target to '' (i.e. 'build') if no target is
881    # explicitly defined
882    IMAGE_GENERATING_TASKS = ['', 'build', 'image', 'populate_sdk_ext']
883
884    # pylint: disable=protected-access
885    # the code will look into the protected variables of the event; no easy way around this
886    # pylint: disable=bad-continuation
887    # we do not follow the python conventions for continuation indentation due to long lines here
888
889    def __init__(self, server, has_build_history = False, brbe = None):
890        self.internal_state = {}
891        self.internal_state['taskdata'] = {}
892        self.internal_state['targets'] = []
893        self.task_order = 0
894        self.autocommit_step = 1
895        self.server = server
896        # we use manual transactions if the database doesn't autocommit on us
897        if not connection.features.autocommits_when_autocommit_is_off:
898            transaction.set_autocommit(False)
899        self.orm_wrapper = ORMWrapper()
900        self.has_build_history = has_build_history
901        self.tmp_dir = self.server.runCommand(["getVariable", "TMPDIR"])[0]
902
903        # this is set for Toaster-triggered builds by localhostbecontroller
904        # via toasterui
905        self.brbe = brbe
906
907        self.project = None
908
909        logger.debug("buildinfohelper: Build info helper inited %s" % vars(self))
910
911
912    ###################
913    ## methods to convert event/external info into objects that the ORM layer uses
914
915    def _ensure_build(self):
916        """
917        Ensure the current build object exists and is up to date with
918        data on the bitbake server
919        """
920        if not 'build' in self.internal_state or not self.internal_state['build']:
921            # create the Build object
922            self.internal_state['build'] = \
923                self.orm_wrapper.get_or_create_build_object(self.brbe)
924
925        build = self.internal_state['build']
926
927        # update missing fields on the Build object with found data
928        build_info = {}
929
930        # set to True if at least one field is going to be set
931        changed = False
932
933        if not build.build_name:
934            build_name = self.server.runCommand(["getVariable", "BUILDNAME"])[0]
935
936            # only reset the build name if the one on the server is actually
937            # a valid value for the build_name field
938            if build_name is not None:
939                build_info['build_name'] = build_name
940                changed = True
941
942        if not build.machine:
943            build_info['machine'] = self.server.runCommand(["getVariable", "MACHINE"])[0]
944            changed = True
945
946        if not build.distro:
947            build_info['distro'] = self.server.runCommand(["getVariable", "DISTRO"])[0]
948            changed = True
949
950        if not build.distro_version:
951            build_info['distro_version'] = self.server.runCommand(["getVariable", "DISTRO_VERSION"])[0]
952            changed = True
953
954        if not build.bitbake_version:
955            build_info['bitbake_version'] = self.server.runCommand(["getVariable", "BB_VERSION"])[0]
956            changed = True
957
958        if changed:
959            self.orm_wrapper.update_build(self.internal_state['build'], build_info)
960
961    def _get_task_information(self, event, recipe):
962        assert 'taskname' in vars(event)
963        self._ensure_build()
964
965        task_information = {}
966        task_information['build'] = self.internal_state['build']
967        task_information['outcome'] = Task.OUTCOME_NA
968        task_information['recipe'] = recipe
969        task_information['task_name'] = event.taskname
970        try:
971            # some tasks don't come with a hash. and that's ok
972            task_information['sstate_checksum'] = event.taskhash
973        except AttributeError:
974            pass
975        return task_information
976
977    def _get_layer_version_for_dependency(self, pathRE):
978        """ Returns the layer in the toaster db that has a full regex
979        match to the pathRE. pathRE - the layer path passed as a regex in the
980        event. It is created in cooker.py as a collection for the layer
981        priorities.
982        """
983        self._ensure_build()
984
985        def _sort_longest_path(layer_version):
986            assert isinstance(layer_version, Layer_Version)
987            return len(layer_version.local_path)
988
989        # Our paths don't append a trailing slash
990        if pathRE.endswith("/"):
991            pathRE = pathRE[:-1]
992
993        p = re.compile(pathRE)
994        path=re.sub(r'[$^]',r'',pathRE)
995        # Heuristics: we always match recipe to the deepest layer path in
996        # the discovered layers
997        for lvo in sorted(self.orm_wrapper.layer_version_objects,
998                          reverse=True, key=_sort_longest_path):
999            if p.fullmatch(os.path.abspath(lvo.local_path)):
1000                return lvo
1001            if lvo.layer.local_source_dir:
1002                if p.fullmatch(os.path.abspath(lvo.layer.local_source_dir)):
1003                    return lvo
1004            if 0 == path.find(lvo.local_path):
1005                # sub-layer path inside existing layer
1006                return lvo
1007
1008        # if we get here, we didn't read layers correctly;
1009        # dump whatever information we have on the error log
1010        logger.warning("Could not match layer dependency for path %s : %s",
1011                       pathRE,
1012                       self.orm_wrapper.layer_version_objects)
1013        return None
1014
1015    def _get_layer_version_for_path(self, path):
1016        self._ensure_build()
1017
1018        def _slkey_interactive(layer_version):
1019            assert isinstance(layer_version, Layer_Version)
1020            return len(layer_version.local_path)
1021
1022        # Heuristics: we always match recipe to the deepest layer path in the discovered layers
1023        for lvo in sorted(self.orm_wrapper.layer_version_objects, reverse=True, key=_slkey_interactive):
1024            # we can match to the recipe file path
1025            if path.startswith(lvo.local_path):
1026                return lvo
1027            if lvo.layer.local_source_dir and \
1028               path.startswith(lvo.layer.local_source_dir):
1029                return lvo
1030
1031        #if we get here, we didn't read layers correctly; dump whatever information we have on the error log
1032        logger.warning("Could not match layer version for recipe path %s : %s", path, self.orm_wrapper.layer_version_objects)
1033
1034        #mockup the new layer
1035        unknown_layer, _ = Layer.objects.get_or_create(name="Unidentified layer", layer_index_url="")
1036        unknown_layer_version_obj, _ = Layer_Version.objects.get_or_create(layer = unknown_layer, build = self.internal_state['build'])
1037
1038        # append it so we don't run into this error again and again
1039        self.orm_wrapper.layer_version_objects.append(unknown_layer_version_obj)
1040
1041        return unknown_layer_version_obj
1042
1043    def _get_recipe_information_from_taskfile(self, taskfile):
1044        localfilepath = taskfile.split(":")[-1]
1045        filepath_flags = ":".join(sorted(taskfile.split(":")[:-1]))
1046        layer_version_obj = self._get_layer_version_for_path(localfilepath)
1047
1048
1049
1050        recipe_info = {}
1051        recipe_info['layer_version'] = layer_version_obj
1052        recipe_info['file_path'] = localfilepath
1053        recipe_info['pathflags'] = filepath_flags
1054
1055        if recipe_info['file_path'].startswith(recipe_info['layer_version'].local_path):
1056            recipe_info['file_path'] = recipe_info['file_path'][len(recipe_info['layer_version'].local_path):].lstrip("/")
1057        else:
1058            raise RuntimeError("Recipe file path %s is not under layer version at %s" % (recipe_info['file_path'], recipe_info['layer_version'].local_path))
1059
1060        return recipe_info
1061
1062    def _get_path_information(self, task_object):
1063        self._ensure_build()
1064
1065        assert isinstance(task_object, Task)
1066        build_stats_format = "{tmpdir}/buildstats/{buildname}/{package}/"
1067        build_stats_path = []
1068
1069        for t in self.internal_state['targets']:
1070            buildname = self.internal_state['build'].build_name
1071            pe, pv = task_object.recipe.version.split(":",1)
1072            if len(pe) > 0:
1073                package = task_object.recipe.name + "-" + pe + "_" + pv
1074            else:
1075                package = task_object.recipe.name + "-" + pv
1076
1077            build_stats_path.append(build_stats_format.format(tmpdir=self.tmp_dir,
1078                                                     buildname=buildname,
1079                                                     package=package))
1080
1081        return build_stats_path
1082
1083
1084    ################################
1085    ## external available methods to store information
1086    @staticmethod
1087    def _get_data_from_event(event):
1088        evdata = None
1089        if '_localdata' in vars(event):
1090            evdata = event._localdata
1091        elif 'data' in vars(event):
1092            evdata = event.data
1093        else:
1094            raise Exception("Event with neither _localdata or data properties")
1095        return evdata
1096
1097    def store_layer_info(self, event):
1098        layerinfos = BuildInfoHelper._get_data_from_event(event)
1099        self.internal_state['lvs'] = {}
1100        for layer in layerinfos:
1101            try:
1102                self.internal_state['lvs'][self.orm_wrapper.get_update_layer_object(layerinfos[layer], self.brbe)] = layerinfos[layer]['version']
1103                self.internal_state['lvs'][self.orm_wrapper.get_update_layer_object(layerinfos[layer], self.brbe)]['local_path'] = layerinfos[layer]['local_path']
1104            except NotExisting as nee:
1105                logger.warning("buildinfohelper: cannot identify layer exception:%s ", nee)
1106
1107    def store_started_build(self):
1108        self._ensure_build()
1109
1110    def save_build_log_file_path(self, build_log_path):
1111        self._ensure_build()
1112
1113        if not self.internal_state['build'].cooker_log_path:
1114            data_dict = {'cooker_log_path': build_log_path}
1115            self.orm_wrapper.update_build(self.internal_state['build'], data_dict)
1116
1117    def save_build_targets(self, event):
1118        self._ensure_build()
1119
1120        # create target information
1121        assert '_pkgs' in vars(event)
1122        target_information = {}
1123        target_information['targets'] = event._pkgs
1124        target_information['build'] = self.internal_state['build']
1125
1126        self.internal_state['targets'] = self.orm_wrapper.get_or_create_targets(target_information)
1127
1128    def save_build_layers_and_variables(self):
1129        self._ensure_build()
1130
1131        build_obj = self.internal_state['build']
1132
1133        # save layer version information for this build
1134        if not 'lvs' in self.internal_state:
1135            logger.error("Layer version information not found; Check if the bitbake server was configured to inherit toaster.bbclass.")
1136        else:
1137            for layer_obj in self.internal_state['lvs']:
1138                self.orm_wrapper.get_update_layer_version_object(build_obj, layer_obj, self.internal_state['lvs'][layer_obj])
1139
1140            del self.internal_state['lvs']
1141
1142        # Save build configuration
1143        data = self.server.runCommand(["getAllKeysWithFlags", ["doc", "func"]])[0]
1144
1145        # convert the paths from absolute to relative to either the build directory or layer checkouts
1146        path_prefixes = []
1147
1148        if self.brbe is not None:
1149            _, be_id = self.brbe.split(":")
1150            be = BuildEnvironment.objects.get(pk = be_id)
1151            path_prefixes.append(be.builddir)
1152
1153        for layer in sorted(self.orm_wrapper.layer_version_objects, key = lambda x:len(x.local_path), reverse=True):
1154            path_prefixes.append(layer.local_path)
1155
1156        # we strip the prefixes
1157        for k in data:
1158            if not bool(data[k]['func']):
1159                for vh in data[k]['history']:
1160                    if not 'documentation.conf' in vh['file']:
1161                        abs_file_name = vh['file']
1162                        for pp in path_prefixes:
1163                            if abs_file_name.startswith(pp + "/"):
1164                                # preserve layer name in relative path
1165                                vh['file']=abs_file_name[pp.rfind("/")+1:]
1166                                break
1167
1168        # save the variables
1169        self.orm_wrapper.save_build_variables(build_obj, data)
1170
1171        return self.brbe
1172
1173    def set_recipes_to_parse(self, num_recipes):
1174        """
1175        Set the number of recipes which need to be parsed for this build.
1176        This is set the first time ParseStarted is received by toasterui.
1177        """
1178        self._ensure_build()
1179        self.internal_state['build'].recipes_to_parse = num_recipes
1180        self.internal_state['build'].save()
1181
1182    def set_recipes_parsed(self, num_recipes):
1183        """
1184        Set the number of recipes parsed so far for this build; this is updated
1185        each time a ParseProgress or ParseCompleted event is received by
1186        toasterui.
1187        """
1188        self._ensure_build()
1189        if num_recipes <= self.internal_state['build'].recipes_to_parse:
1190            self.internal_state['build'].recipes_parsed = num_recipes
1191            self.internal_state['build'].save()
1192
1193    def update_target_image_file(self, event):
1194        evdata = BuildInfoHelper._get_data_from_event(event)
1195
1196        for t in self.internal_state['targets']:
1197            if t.is_image:
1198                output_files = list(evdata.keys())
1199                for output in output_files:
1200                    if t.target in output and 'rootfs' in output and not output.endswith(".manifest"):
1201                        self.orm_wrapper.save_target_image_file_information(t, output, evdata[output])
1202
1203    def update_artifact_image_file(self, event):
1204        self._ensure_build()
1205        evdata = BuildInfoHelper._get_data_from_event(event)
1206        for artifact_path in evdata.keys():
1207            self.orm_wrapper.save_artifact_information(
1208                self.internal_state['build'], artifact_path,
1209                evdata[artifact_path])
1210
1211    def update_build_information(self, event, errors, warnings, taskfailures):
1212        self._ensure_build()
1213        self.orm_wrapper.update_build_stats_and_outcome(
1214            self.internal_state['build'], errors, warnings, taskfailures)
1215
1216    def store_started_task(self, event):
1217        assert isinstance(event, (bb.runqueue.sceneQueueTaskStarted, bb.runqueue.runQueueTaskStarted, bb.runqueue.runQueueTaskSkipped))
1218        assert 'taskfile' in vars(event)
1219        localfilepath = event.taskfile.split(":")[-1]
1220        assert localfilepath.startswith("/")
1221
1222        identifier = event.taskfile + ":" + event.taskname
1223
1224        recipe_information = self._get_recipe_information_from_taskfile(event.taskfile)
1225        recipe = self.orm_wrapper.get_update_recipe_object(recipe_information, True)
1226
1227        task_information = self._get_task_information(event, recipe)
1228        task_information['outcome'] = Task.OUTCOME_NA
1229
1230        if isinstance(event, bb.runqueue.runQueueTaskSkipped):
1231            assert 'reason' in vars(event)
1232            task_information['task_executed'] = False
1233            if event.reason == "covered":
1234                task_information['outcome'] = Task.OUTCOME_COVERED
1235            if event.reason == "existing":
1236                task_information['outcome'] = Task.OUTCOME_PREBUILT
1237        else:
1238            task_information['task_executed'] = True
1239            if 'noexec' in vars(event) and event.noexec:
1240                task_information['task_executed'] = False
1241                task_information['outcome'] = Task.OUTCOME_EMPTY
1242                task_information['script_type'] = Task.CODING_NA
1243
1244        # do not assign order numbers to scene tasks
1245        if not isinstance(event, bb.runqueue.sceneQueueTaskStarted):
1246            self.task_order += 1
1247            task_information['order'] = self.task_order
1248
1249        self.orm_wrapper.get_update_task_object(task_information)
1250
1251        self.internal_state['taskdata'][identifier] = {
1252                        'outcome': task_information['outcome'],
1253                    }
1254
1255
1256    def store_tasks_stats(self, event):
1257        self._ensure_build()
1258        task_data = BuildInfoHelper._get_data_from_event(event)
1259
1260        for (task_file, task_name, task_stats, recipe_name) in task_data:
1261            build = self.internal_state['build']
1262            self.orm_wrapper.update_task_object(build, task_name, recipe_name, task_stats)
1263
1264    def update_and_store_task(self, event):
1265        assert 'taskfile' in vars(event)
1266        localfilepath = event.taskfile.split(":")[-1]
1267        assert localfilepath.startswith("/")
1268
1269        identifier = event.taskfile + ":" + event.taskname
1270        if not identifier in self.internal_state['taskdata']:
1271            if isinstance(event, bb.build.TaskBase):
1272                # we do a bit of guessing
1273                candidates = [x for x in self.internal_state['taskdata'].keys() if x.endswith(identifier)]
1274                if len(candidates) == 1:
1275                    identifier = candidates[0]
1276                elif len(candidates) > 1 and hasattr(event,'_package'):
1277                    if 'native-' in event._package:
1278                        identifier = 'native:' + identifier
1279                    if 'nativesdk-' in event._package:
1280                        identifier = 'nativesdk:' + identifier
1281                    candidates = [x for x in self.internal_state['taskdata'].keys() if x.endswith(identifier)]
1282                    if len(candidates) == 1:
1283                        identifier = candidates[0]
1284
1285        assert identifier in self.internal_state['taskdata']
1286        identifierlist = identifier.split(":")
1287        realtaskfile = ":".join(identifierlist[0:len(identifierlist)-1])
1288        recipe_information = self._get_recipe_information_from_taskfile(realtaskfile)
1289        recipe = self.orm_wrapper.get_update_recipe_object(recipe_information, True)
1290        task_information = self._get_task_information(event,recipe)
1291
1292        task_information['outcome'] = self.internal_state['taskdata'][identifier]['outcome']
1293
1294        if 'logfile' in vars(event):
1295            task_information['logfile'] = event.logfile
1296
1297        if '_message' in vars(event):
1298            task_information['message'] = event._message
1299
1300        if 'taskflags' in vars(event):
1301            # with TaskStarted, we get even more information
1302            if 'python' in event.taskflags.keys() and event.taskflags['python'] == '1':
1303                task_information['script_type'] = Task.CODING_PYTHON
1304            else:
1305                task_information['script_type'] = Task.CODING_SHELL
1306
1307        if task_information['outcome'] == Task.OUTCOME_NA:
1308            if isinstance(event, (bb.runqueue.runQueueTaskCompleted, bb.runqueue.sceneQueueTaskCompleted)):
1309                task_information['outcome'] = Task.OUTCOME_SUCCESS
1310                del self.internal_state['taskdata'][identifier]
1311
1312            if isinstance(event, (bb.runqueue.runQueueTaskFailed, bb.runqueue.sceneQueueTaskFailed)):
1313                task_information['outcome'] = Task.OUTCOME_FAILED
1314                del self.internal_state['taskdata'][identifier]
1315
1316        if not connection.features.autocommits_when_autocommit_is_off:
1317            # we force a sync point here, to get the progress bar to show
1318            if self.autocommit_step % 3 == 0:
1319                transaction.set_autocommit(True)
1320                transaction.set_autocommit(False)
1321            self.autocommit_step += 1
1322
1323        self.orm_wrapper.get_update_task_object(task_information, True) # must exist
1324
1325
1326    def store_missed_state_tasks(self, event):
1327        for (fn, taskname, taskhash, sstatefile) in BuildInfoHelper._get_data_from_event(event)['missed']:
1328
1329            # identifier = fn + taskname + "_setscene"
1330            recipe_information = self._get_recipe_information_from_taskfile(fn)
1331            recipe = self.orm_wrapper.get_update_recipe_object(recipe_information)
1332            mevent = MockEvent()
1333            mevent.taskname = taskname
1334            mevent.taskhash = taskhash
1335            task_information = self._get_task_information(mevent,recipe)
1336
1337            task_information['start_time'] = timezone.now()
1338            task_information['outcome'] = Task.OUTCOME_NA
1339            task_information['sstate_checksum'] = taskhash
1340            task_information['sstate_result'] = Task.SSTATE_MISS
1341            task_information['path_to_sstate_obj'] = sstatefile
1342
1343            self.orm_wrapper.get_update_task_object(task_information)
1344
1345        for (fn, taskname, taskhash, sstatefile) in BuildInfoHelper._get_data_from_event(event)['found']:
1346
1347            # identifier = fn + taskname + "_setscene"
1348            recipe_information = self._get_recipe_information_from_taskfile(fn)
1349            recipe = self.orm_wrapper.get_update_recipe_object(recipe_information)
1350            mevent = MockEvent()
1351            mevent.taskname = taskname
1352            mevent.taskhash = taskhash
1353            task_information = self._get_task_information(mevent,recipe)
1354
1355            task_information['path_to_sstate_obj'] = sstatefile
1356
1357            self.orm_wrapper.get_update_task_object(task_information)
1358
1359
1360    def store_target_package_data(self, event):
1361        self._ensure_build()
1362
1363        # for all image targets
1364        for target in self.internal_state['targets']:
1365            if target.is_image:
1366                pkgdata = BuildInfoHelper._get_data_from_event(event)['pkgdata']
1367                imgdata = BuildInfoHelper._get_data_from_event(event)['imgdata'].get(target.target, {})
1368                filedata = BuildInfoHelper._get_data_from_event(event)['filedata'].get(target.target, {})
1369
1370                try:
1371                    self.orm_wrapper.save_target_package_information(self.internal_state['build'], target, imgdata, pkgdata, self.internal_state['recipes'], built_package=True)
1372                    self.orm_wrapper.save_target_package_information(self.internal_state['build'], target, imgdata.copy(), pkgdata, self.internal_state['recipes'], built_package=False)
1373                except KeyError as e:
1374                    logger.warning("KeyError in save_target_package_information"
1375                                   "%s ", e)
1376
1377                # only try to find files in the image if the task for this
1378                # target is one which produces image files; otherwise, the old
1379                # list of files in the files-in-image.txt file will be
1380                # appended to the target even if it didn't produce any images
1381                if target.task in BuildInfoHelper.IMAGE_GENERATING_TASKS:
1382                    try:
1383                        self.orm_wrapper.save_target_file_information(self.internal_state['build'], target, filedata)
1384                    except KeyError as e:
1385                        logger.warning("KeyError in save_target_file_information"
1386                                       "%s ", e)
1387
1388
1389
1390    def cancel_cli_build(self):
1391        """
1392        If a build is currently underway, set its state to CANCELLED;
1393        note that this only gets called for command line builds which are
1394        interrupted, so it doesn't touch any BuildRequest objects
1395        """
1396        self._ensure_build()
1397        self.internal_state['build'].outcome = Build.CANCELLED
1398        self.internal_state['build'].save()
1399        signal_runbuilds()
1400
1401    def store_dependency_information(self, event):
1402        assert '_depgraph' in vars(event)
1403        assert 'layer-priorities' in event._depgraph
1404        assert 'pn' in event._depgraph
1405        assert 'tdepends' in event._depgraph
1406
1407        errormsg = ""
1408
1409        # save layer version priorities
1410        if 'layer-priorities' in event._depgraph.keys():
1411            for lv in event._depgraph['layer-priorities']:
1412                (_, path, _, priority) = lv
1413                layer_version_obj = self._get_layer_version_for_dependency(path)
1414                if layer_version_obj:
1415                    layer_version_obj.priority = priority
1416                    layer_version_obj.save()
1417
1418        # save recipe information
1419        self.internal_state['recipes'] = {}
1420        for pn in event._depgraph['pn']:
1421
1422            file_name = event._depgraph['pn'][pn]['filename'].split(":")[-1]
1423            pathflags = ":".join(sorted(event._depgraph['pn'][pn]['filename'].split(":")[:-1]))
1424            layer_version_obj = self._get_layer_version_for_path(file_name)
1425
1426            assert layer_version_obj is not None
1427
1428            recipe_info = {}
1429            recipe_info['name'] = pn
1430            recipe_info['layer_version'] = layer_version_obj
1431
1432            if 'version' in event._depgraph['pn'][pn]:
1433                recipe_info['version'] = event._depgraph['pn'][pn]['version'].lstrip(":")
1434
1435            if 'summary' in event._depgraph['pn'][pn]:
1436                recipe_info['summary'] = event._depgraph['pn'][pn]['summary']
1437
1438            if 'license' in event._depgraph['pn'][pn]:
1439                recipe_info['license'] = event._depgraph['pn'][pn]['license']
1440
1441            if 'description' in event._depgraph['pn'][pn]:
1442                recipe_info['description'] = event._depgraph['pn'][pn]['description']
1443
1444            if 'section' in event._depgraph['pn'][pn]:
1445                recipe_info['section'] = event._depgraph['pn'][pn]['section']
1446
1447            if 'homepage' in event._depgraph['pn'][pn]:
1448                recipe_info['homepage'] = event._depgraph['pn'][pn]['homepage']
1449
1450            if 'bugtracker' in event._depgraph['pn'][pn]:
1451                recipe_info['bugtracker'] = event._depgraph['pn'][pn]['bugtracker']
1452
1453            recipe_info['file_path'] = file_name
1454            recipe_info['pathflags'] = pathflags
1455
1456            if recipe_info['file_path'].startswith(recipe_info['layer_version'].local_path):
1457                recipe_info['file_path'] = recipe_info['file_path'][len(recipe_info['layer_version'].local_path):].lstrip("/")
1458            else:
1459                raise RuntimeError("Recipe file path %s is not under layer version at %s" % (recipe_info['file_path'], recipe_info['layer_version'].local_path))
1460
1461            recipe = self.orm_wrapper.get_update_recipe_object(recipe_info)
1462            recipe.is_image = False
1463            if 'inherits' in event._depgraph['pn'][pn].keys():
1464                for cls in event._depgraph['pn'][pn]['inherits']:
1465                    if cls.endswith('/image.bbclass'):
1466                        recipe.is_image = True
1467                        recipe_info['is_image'] = True
1468                        # Save the is_image state to the relevant recipe objects
1469                        self.orm_wrapper.get_update_recipe_object(recipe_info)
1470                        break
1471            if recipe.is_image:
1472                for t in self.internal_state['targets']:
1473                    if pn == t.target:
1474                        t.is_image = True
1475                        t.save()
1476            self.internal_state['recipes'][pn] = recipe
1477
1478        # we'll not get recipes for key w/ values listed in ASSUME_PROVIDED
1479
1480        assume_provided = self.server.runCommand(["getVariable", "ASSUME_PROVIDED"])[0].split()
1481
1482        # save recipe dependency
1483        # buildtime
1484        recipedeps_objects = []
1485        for recipe in event._depgraph['depends']:
1486           target = self.internal_state['recipes'][recipe]
1487           for dep in event._depgraph['depends'][recipe]:
1488                if dep in assume_provided:
1489                    continue
1490                via = None
1491                if 'providermap' in event._depgraph and dep in event._depgraph['providermap']:
1492                    deprecipe = event._depgraph['providermap'][dep][0]
1493                    dependency = self.internal_state['recipes'][deprecipe]
1494                    via = Provides.objects.get_or_create(name=dep,
1495                                                         recipe=dependency)[0]
1496                elif dep in self.internal_state['recipes']:
1497                    dependency = self.internal_state['recipes'][dep]
1498                else:
1499                    errormsg += "  stpd: KeyError saving recipe dependency for %s, %s \n" % (recipe, dep)
1500                    continue
1501                recipe_dep = Recipe_Dependency(recipe=target,
1502                                               depends_on=dependency,
1503                                               via=via,
1504                                               dep_type=Recipe_Dependency.TYPE_DEPENDS)
1505                recipedeps_objects.append(recipe_dep)
1506
1507        Recipe_Dependency.objects.bulk_create(recipedeps_objects)
1508
1509        # save all task information
1510        def _save_a_task(taskdesc):
1511            spec = re.split(r'\.', taskdesc)
1512            pn = ".".join(spec[0:-1])
1513            taskname = spec[-1]
1514            e = event
1515            e.taskname = pn
1516            recipe = self.internal_state['recipes'][pn]
1517            task_info = self._get_task_information(e, recipe)
1518            task_info['task_name'] = taskname
1519            task_obj = self.orm_wrapper.get_update_task_object(task_info)
1520            return task_obj
1521
1522        # create tasks
1523        tasks = {}
1524        for taskdesc in event._depgraph['tdepends']:
1525            tasks[taskdesc] = _save_a_task(taskdesc)
1526
1527        # create dependencies between tasks
1528        taskdeps_objects = []
1529        for taskdesc in event._depgraph['tdepends']:
1530            target = tasks[taskdesc]
1531            for taskdep in event._depgraph['tdepends'][taskdesc]:
1532                if taskdep not in tasks:
1533                    # Fetch tasks info is not collected previously
1534                    dep = _save_a_task(taskdep)
1535                else:
1536                    dep = tasks[taskdep]
1537                taskdeps_objects.append(Task_Dependency( task = target, depends_on = dep ))
1538        Task_Dependency.objects.bulk_create(taskdeps_objects)
1539
1540        if len(errormsg) > 0:
1541            logger.warning("buildinfohelper: dependency info not identify recipes: \n%s", errormsg)
1542
1543
1544    def store_build_package_information(self, event):
1545        self._ensure_build()
1546
1547        package_info = BuildInfoHelper._get_data_from_event(event)
1548        self.orm_wrapper.save_build_package_information(
1549            self.internal_state['build'],
1550            package_info,
1551            self.internal_state['recipes'],
1552            built_package=True)
1553
1554        self.orm_wrapper.save_build_package_information(
1555            self.internal_state['build'],
1556            package_info,
1557            self.internal_state['recipes'],
1558            built_package=False)
1559
1560    def _store_build_done(self, errorcode):
1561        logger.info("Build exited with errorcode %d", errorcode)
1562
1563        if not self.brbe:
1564            return
1565
1566        br_id, be_id = self.brbe.split(":")
1567
1568        br = BuildRequest.objects.get(pk = br_id)
1569
1570        # if we're 'done' because we got cancelled update the build outcome
1571        if br.state == BuildRequest.REQ_CANCELLING:
1572            logger.info("Build cancelled")
1573            br.build.outcome = Build.CANCELLED
1574            br.build.save()
1575            self.internal_state['build'] = br.build
1576            errorcode = 0
1577
1578        if errorcode == 0:
1579            # request archival of the project artifacts
1580            br.state = BuildRequest.REQ_COMPLETED
1581        else:
1582            br.state = BuildRequest.REQ_FAILED
1583        br.save()
1584
1585        be = BuildEnvironment.objects.get(pk = be_id)
1586        be.lock = BuildEnvironment.LOCK_FREE
1587        be.save()
1588        signal_runbuilds()
1589
1590    def store_log_error(self, text):
1591        mockevent = MockEvent()
1592        mockevent.levelno = formatter.ERROR
1593        mockevent.msg = text
1594        mockevent.pathname = '-- None'
1595        mockevent.lineno = LogMessage.ERROR
1596        self.store_log_event(mockevent)
1597
1598    def store_log_exception(self, text, backtrace = ""):
1599        mockevent = MockEvent()
1600        mockevent.levelno = -1
1601        mockevent.msg = text
1602        mockevent.pathname = backtrace
1603        mockevent.lineno = -1
1604        self.store_log_event(mockevent)
1605
1606    def store_log_event(self, event,cli_backlog=True):
1607        self._ensure_build()
1608
1609        if event.levelno < formatter.WARNING:
1610            return
1611
1612        # early return for CLI builds
1613        if cli_backlog and self.brbe is None:
1614            if not 'backlog' in self.internal_state:
1615                self.internal_state['backlog'] = []
1616            self.internal_state['backlog'].append(event)
1617            return
1618
1619        if 'backlog' in self.internal_state:
1620            # if we have a backlog of events, do our best to save them here
1621            if len(self.internal_state['backlog']):
1622                tempevent = self.internal_state['backlog'].pop()
1623                logger.debug("buildinfohelper: Saving stored event %s "
1624                             % tempevent)
1625                self.store_log_event(tempevent,cli_backlog)
1626            else:
1627                logger.info("buildinfohelper: All events saved")
1628                del self.internal_state['backlog']
1629
1630        log_information = {}
1631        log_information['build'] = self.internal_state['build']
1632        if event.levelno == formatter.CRITICAL:
1633            log_information['level'] = LogMessage.CRITICAL
1634        elif event.levelno == formatter.ERROR:
1635            log_information['level'] = LogMessage.ERROR
1636        elif event.levelno == formatter.WARNING:
1637            log_information['level'] = LogMessage.WARNING
1638        elif event.levelno == -2:   # toaster self-logging
1639            log_information['level'] = -2
1640        else:
1641            log_information['level'] = LogMessage.INFO
1642
1643        log_information['message'] = event.getMessage()
1644        log_information['pathname'] = event.pathname
1645        log_information['lineno'] = event.lineno
1646        logger.info("Logging error 2: %s", log_information)
1647
1648        self.orm_wrapper.create_logmessage(log_information)
1649
1650    def _get_filenames_from_image_license(self, image_license_manifest_path):
1651        """
1652        Find the FILES line in the image_license.manifest file,
1653        which has the basenames of the bzImage and modules files
1654        in this format:
1655        FILES: bzImage--4.4.11+git0+3a5f494784_53e84104c5-r0-qemux86-20160603165040.bin modules--4.4.11+git0+3a5f494784_53e84104c5-r0-qemux86-20160603165040.tgz
1656        """
1657        files = []
1658        with open(image_license_manifest_path) as image_license:
1659            for line in image_license:
1660                if line.startswith('FILES'):
1661                    files_str = line.split(':')[1].strip()
1662                    files_str = re.sub(r' {2,}', ' ', files_str)
1663
1664                    # ignore lines like "FILES:" with no filenames
1665                    if files_str:
1666                        files += files_str.split(' ')
1667        return files
1668
1669    def _endswith(self, str_to_test, endings):
1670        """
1671        Returns True if str ends with one of the strings in the list
1672        endings, False otherwise
1673        """
1674        endswith = False
1675        for ending in endings:
1676            if str_to_test.endswith(ending):
1677                endswith = True
1678                break
1679        return endswith
1680
1681    def scan_task_artifacts(self, event):
1682        """
1683        The 'TaskArtifacts' event passes the manifest file content for the
1684        tasks 'do_deploy', 'do_image_complete', 'do_populate_sdk', and
1685        'do_populate_sdk_ext'. The first two will be implemented later.
1686        """
1687        task_vars = BuildInfoHelper._get_data_from_event(event)
1688        task_name = task_vars['task'][task_vars['task'].find(':')+1:]
1689        task_artifacts = task_vars['artifacts']
1690
1691        if task_name in ['do_populate_sdk', 'do_populate_sdk_ext']:
1692            targets = [target for target in self.internal_state['targets'] \
1693                if target.task == task_name[3:]]
1694            if not targets:
1695                logger.warning("scan_task_artifacts: SDK targets not found: %s\n", task_name)
1696                return
1697            for artifact_path in task_artifacts:
1698                if not os.path.isfile(artifact_path):
1699                    logger.warning("scan_task_artifacts: artifact file not found: %s\n", artifact_path)
1700                    continue
1701                for target in targets:
1702                    # don't record the file if it's already been added
1703                    # to this target
1704                    matching_files = TargetSDKFile.objects.filter(
1705                        target=target, file_name=artifact_path)
1706                    if matching_files.count() == 0:
1707                        artifact_size = os.stat(artifact_path).st_size
1708                        self.orm_wrapper.save_target_sdk_file(
1709                            target, artifact_path, artifact_size)
1710
1711    def _get_image_files(self, deploy_dir_image, image_name, image_file_extensions):
1712        """
1713        Find files in deploy_dir_image whose basename starts with the
1714        string image_name and ends with one of the strings in
1715        image_file_extensions.
1716
1717        Returns a list of file dictionaries like
1718
1719        [
1720            {
1721                'path': '/path/to/image/file',
1722                'size': <file size in bytes>
1723            }
1724        ]
1725        """
1726        image_files = []
1727
1728        for dirpath, _, filenames in os.walk(deploy_dir_image):
1729            for filename in filenames:
1730                if filename.startswith(image_name) and \
1731                self._endswith(filename, image_file_extensions):
1732                    image_file_path = os.path.join(dirpath, filename)
1733                    image_file_size = os.stat(image_file_path).st_size
1734
1735                    image_files.append({
1736                        'path': image_file_path,
1737                        'size': image_file_size
1738                    })
1739
1740        return image_files
1741
1742    def scan_image_artifacts(self):
1743        """
1744        Scan for built image artifacts in DEPLOY_DIR_IMAGE and associate them
1745        with a Target object in self.internal_state['targets'].
1746
1747        We have two situations to handle:
1748
1749        1. This is the first time a target + machine has been built, so
1750        add files from the DEPLOY_DIR_IMAGE to the target.
1751
1752        OR
1753
1754        2. There are no new files for the target (they were already produced by
1755        a previous build), so copy them from the most recent previous build with
1756        the same target, task and machine.
1757        """
1758        deploy_dir_image = \
1759            self.server.runCommand(['getVariable', 'DEPLOY_DIR_IMAGE'])[0]
1760
1761        # if there's no DEPLOY_DIR_IMAGE, there aren't going to be
1762        # any image artifacts, so we can return immediately
1763        if not deploy_dir_image:
1764            return
1765
1766        buildname = self.server.runCommand(['getVariable', 'BUILDNAME'])[0]
1767        machine = self.server.runCommand(['getVariable', 'MACHINE'])[0]
1768        image_name = self.server.runCommand(['getVariable', 'IMAGE_NAME'])[0]
1769
1770        # location of the manifest files for this build;
1771        # note that this file is only produced if an image is produced
1772        license_directory = \
1773            self.server.runCommand(['getVariable', 'LICENSE_DIRECTORY'])[0]
1774
1775        # file name extensions for image files
1776        image_file_extensions_unique = {}
1777        image_fstypes = self.server.runCommand(
1778            ['getVariable', 'IMAGE_FSTYPES'])[0]
1779        if image_fstypes is not None:
1780            image_types_str = image_fstypes.strip()
1781            image_file_extensions = re.sub(r' {2,}', ' ', image_types_str)
1782            image_file_extensions_unique = set(image_file_extensions.split(' '))
1783
1784        targets = self.internal_state['targets']
1785
1786        # filter out anything which isn't an image target
1787        image_targets = [target for target in targets if target.is_image]
1788
1789        for image_target in image_targets:
1790            # this is set to True if we find at least one file relating to
1791            # this target; if this remains False after the scan, we copy the
1792            # files from the most-recent Target with the same target + machine
1793            # onto this Target instead
1794            has_files = False
1795
1796            # we construct this because by the time we reach
1797            # BuildCompleted, this has reset to
1798            # 'defaultpkgname-<MACHINE>-<BUILDNAME>';
1799            # we need to change it to
1800            # <TARGET>-<MACHINE>-<BUILDNAME>
1801            real_image_name = re.sub(r'^defaultpkgname', image_target.target,
1802                image_name)
1803
1804            image_license_manifest_path = os.path.join(
1805                license_directory,
1806                real_image_name,
1807                'image_license.manifest')
1808
1809            image_package_manifest_path = os.path.join(
1810                license_directory,
1811                real_image_name,
1812                'image_license.manifest')
1813
1814            # if image_license.manifest exists, we can read the names of
1815            # bzImage, modules etc. files for this build from it, then look for
1816            # them in the DEPLOY_DIR_IMAGE; note that this file is only produced
1817            # if an image file was produced
1818            if os.path.isfile(image_license_manifest_path):
1819                has_files = True
1820
1821                basenames = self._get_filenames_from_image_license(
1822                    image_license_manifest_path)
1823
1824                for basename in basenames:
1825                    artifact_path = os.path.join(deploy_dir_image, basename)
1826                    if not os.path.exists(artifact_path):
1827                        logger.warning("artifact %s doesn't exist, skipping" % artifact_path)
1828                        continue
1829                    artifact_size = os.stat(artifact_path).st_size
1830
1831                    # note that the artifact will only be saved against this
1832                    # build if it hasn't been already
1833                    self.orm_wrapper.save_target_kernel_file(image_target,
1834                        artifact_path, artifact_size)
1835
1836                # store the license manifest path on the target
1837                # (this file is also created any time an image file is created)
1838                license_manifest_path = os.path.join(license_directory,
1839                    real_image_name, 'license.manifest')
1840
1841                self.orm_wrapper.update_target_set_license_manifest(
1842                    image_target, license_manifest_path)
1843
1844                # store the package manifest path on the target (this file
1845                # is created any time an image file is created)
1846                package_manifest_path = os.path.join(deploy_dir_image,
1847                    real_image_name + '.rootfs.manifest')
1848
1849                if os.path.exists(package_manifest_path):
1850                    self.orm_wrapper.update_target_set_package_manifest(
1851                        image_target, package_manifest_path)
1852
1853            # scan the directory for image files relating to this build
1854            # (via real_image_name); note that we don't have to set
1855            # has_files = True, as searching for the license manifest file
1856            # will already have set it to true if at least one image file was
1857            # produced; note that the real_image_name includes BUILDNAME, which
1858            # in turn includes a timestamp; so if no files were produced for
1859            # this timestamp (i.e. the build reused existing image files already
1860            # in the directory), no files will be recorded against this target
1861            image_files = self._get_image_files(deploy_dir_image,
1862                real_image_name, image_file_extensions_unique)
1863
1864            for image_file in image_files:
1865                self.orm_wrapper.save_target_image_file_information(
1866                    image_target, image_file['path'], image_file['size'])
1867
1868            if not has_files:
1869                # copy image files and build artifacts from the
1870                # most-recently-built Target with the
1871                # same target + machine as this Target; also copy the license
1872                # manifest path, as that is not treated as an artifact and needs
1873                # to be set separately
1874                similar_target = \
1875                    self.orm_wrapper.get_similar_target_with_image_files(
1876                        image_target)
1877
1878                if similar_target:
1879                    logger.info('image artifacts for target %s cloned from ' \
1880                        'target %s' % (image_target.pk, similar_target.pk))
1881                    self.orm_wrapper.clone_image_artifacts(similar_target,
1882                        image_target)
1883
1884    def _get_sdk_targets(self):
1885        """
1886        Return targets which could generate SDK artifacts, i.e.
1887        "do_populate_sdk" and "do_populate_sdk_ext".
1888        """
1889        return [target for target in self.internal_state['targets'] \
1890            if target.task in ['populate_sdk', 'populate_sdk_ext']]
1891
1892    def scan_sdk_artifacts(self, event):
1893        """
1894        Note that we have to intercept an SDKArtifactInfo event from
1895        toaster.bbclass (via toasterui) to get hold of the SDK variables we
1896        need to be able to scan for files accurately: this is because
1897        variables like TOOLCHAIN_OUTPUTNAME have reset to None by the time
1898        BuildCompleted is fired by bitbake, so we have to get those values
1899        while the build is still in progress.
1900
1901        For populate_sdk_ext, this runs twice, with two different
1902        TOOLCHAIN_OUTPUTNAME settings, each of which will capture some of the
1903        files in the SDK output directory.
1904        """
1905        sdk_vars = BuildInfoHelper._get_data_from_event(event)
1906        toolchain_outputname = sdk_vars['TOOLCHAIN_OUTPUTNAME']
1907
1908        # targets which might have created SDK artifacts
1909        sdk_targets = self._get_sdk_targets()
1910
1911        # location of SDK artifacts
1912        tmpdir = self.server.runCommand(['getVariable', 'TMPDIR'])[0]
1913        sdk_dir = os.path.join(tmpdir, 'deploy', 'sdk')
1914
1915        # all files in the SDK directory
1916        artifacts = []
1917        for dir_path, _, filenames in os.walk(sdk_dir):
1918            for filename in filenames:
1919                full_path = os.path.join(dir_path, filename)
1920                if not os.path.islink(full_path):
1921                    artifacts.append(full_path)
1922
1923        for sdk_target in sdk_targets:
1924            # find files in the SDK directory which haven't already been
1925            # recorded against a Target and whose basename matches
1926            # TOOLCHAIN_OUTPUTNAME
1927            for artifact_path in artifacts:
1928                basename = os.path.basename(artifact_path)
1929
1930                toolchain_match = basename.startswith(toolchain_outputname)
1931
1932                # files which match the name of the target which produced them;
1933                # for example,
1934                # poky-glibc-x86_64-core-image-sato-i586-toolchain-ext-2.1+snapshot.sh
1935                target_match = re.search(sdk_target.target, basename)
1936
1937                # targets which produce "*-nativesdk-*" files
1938                is_ext_sdk_target = sdk_target.task in \
1939                    ['do_populate_sdk_ext', 'populate_sdk_ext']
1940
1941                # SDK files which don't match the target name, i.e.
1942                # x86_64-nativesdk-libc.*
1943                # poky-glibc-x86_64-buildtools-tarball-i586-buildtools-nativesdk-standalone-2.1+snapshot*
1944                is_ext_sdk_file = re.search('-nativesdk-', basename)
1945
1946                file_from_target = (toolchain_match and target_match) or \
1947                    (is_ext_sdk_target and is_ext_sdk_file)
1948
1949                if file_from_target:
1950                    # don't record the file if it's already been added to this
1951                    # target
1952                    matching_files = TargetSDKFile.objects.filter(
1953                        target=sdk_target, file_name=artifact_path)
1954
1955                    if matching_files.count() == 0:
1956                        artifact_size = os.stat(artifact_path).st_size
1957
1958                        self.orm_wrapper.save_target_sdk_file(
1959                            sdk_target, artifact_path, artifact_size)
1960
1961    def clone_required_sdk_artifacts(self):
1962        """
1963        If an SDK target doesn't have any SDK artifacts, this means that
1964        the postfuncs of populate_sdk or populate_sdk_ext didn't fire, which
1965        in turn means that the targets of this build didn't generate any new
1966        artifacts.
1967
1968        In this case, clone SDK artifacts for targets in the current build
1969        from existing targets for this build.
1970        """
1971        sdk_targets = self._get_sdk_targets()
1972        for sdk_target in sdk_targets:
1973            # only clone for SDK targets which have no TargetSDKFiles yet
1974            if sdk_target.targetsdkfile_set.all().count() == 0:
1975                similar_target = \
1976                    self.orm_wrapper.get_similar_target_with_sdk_files(
1977                        sdk_target)
1978                if similar_target:
1979                    logger.info('SDK artifacts for target %s cloned from ' \
1980                        'target %s' % (sdk_target.pk, similar_target.pk))
1981                    self.orm_wrapper.clone_sdk_artifacts(similar_target,
1982                        sdk_target)
1983
1984    def close(self, errorcode):
1985        self._store_build_done(errorcode)
1986
1987        if 'backlog' in self.internal_state:
1988            # we save missed events in the database for the current build
1989            tempevent = self.internal_state['backlog'].pop()
1990            # Do not skip command line build events
1991            self.store_log_event(tempevent,False)
1992
1993        if not connection.features.autocommits_when_autocommit_is_off:
1994            transaction.set_autocommit(True)
1995
1996        # unset the brbe; this is to prevent subsequent command-line builds
1997        # being incorrectly attached to the previous Toaster-triggered build;
1998        # see https://bugzilla.yoctoproject.org/show_bug.cgi?id=9021
1999        self.brbe = None
2000
2001        # unset the internal Build object to prevent it being reused for the
2002        # next build
2003        self.internal_state['build'] = None
2004