# -*- coding: utf-8 -*- # Fetch layer repositories and update layer index database # # Copyright (C) 2013 - 2015 Intel Corporation # Author: Paul Eggleton # Contributor: Aníbal Limón # # Licensed under the MIT license, see COPYING.MIT for details import re from datetime import datetime from distutils.version import LooseVersion import settings from django.db import transaction import utils import recipeparse from layerindex.models import LayerItem, LayerBranch, Recipe, \ RecipeFileDependency, Machine, BBAppend, BBClass # Ensure PythonGit is installed (buildhistory_analysis needs it) try: import git if LooseVersion(git.__version__) < '0.3.1': logger.error("Version of GitPython is too old, please install" \ " GitPython (python-git) 0.3.1 or later in order to use this script") sys.exit(1) except ImportError: logger.error("Please install PythonGit 0.3.1 or later in order to use this script") sys.exit(1) class LayerindexUpdater: def __init__(self, _options, _fetchdir, _layerquery, _fetchedrepos, _failedrepos, _logger): global options global fetchdir global layerquery global fetchedrepos global failedrepos global logger options = _options fetchdir = _fetchdir layerquery = _layerquery fetchedrepos = _fetchedrepos failedrepos = _failedrepos logger = _logger def run(self, tinfoil): # Process and extract data from each layer for layer in layerquery: transaction.enter_transaction_management() transaction.managed(True) try: urldir = layer.get_fetch_dir() repodir = os.path.join(fetchdir, urldir) if layer.vcs_url in failedrepos: logger.info("Skipping update of layer %s as fetch of repository %s failed" % (layer.name, layer.vcs_url)) transaction.rollback() continue layerbranch = layer.get_layerbranch(options.branch) branchname = options.branch branchdesc = options.branch if layerbranch: if layerbranch.actual_branch: branchname = layerbranch.actual_branch branchdesc = "%s (%s)" % (options.branch, branchname) # Collect repo info repo = git.Repo(repodir) assert repo.bare == False try: if options.nocheckout: topcommit = repo.commit('HEAD') else: topcommit = repo.commit('origin/%s' % branchname) except: if layerbranch: logger.error("Failed update of layer %s - branch %s no longer exists" % (layer.name, branchdesc)) else: logger.info("Skipping update of layer %s - branch %s doesn't exist" % (layer.name, branchdesc)) transaction.rollback() continue newbranch = False if not layerbranch: # LayerBranch doesn't exist for this branch, create it newbranch = True layerbranch = LayerBranch() layerbranch.layer = layer layerbranch.branch = branch layerbranch_source = layer.get_layerbranch('master') if not layerbranch_source: layerbranch_source = layer.get_layerbranch(None) if layerbranch_source: layerbranch.vcs_subdir = layerbranch_source.vcs_subdir layerbranch.save() if layerbranch_source: for maintainer in layerbranch_source.layermaintainer_set.all(): maintainer.pk = None maintainer.id = None maintainer.layerbranch = layerbranch maintainer.save() for dep in layerbranch_source.dependencies_set.all(): dep.pk = None dep.id = None dep.layerbranch = layerbranch dep.save() if layerbranch.vcs_subdir and not options.nocheckout: # Find latest commit in subdirectory # A bit odd to do it this way but apparently there's no other way in the GitPython API topcommit = next(repo.iter_commits('origin/%s' % branchname, paths=layerbranch.vcs_subdir), None) if not topcommit: # This will error out if the directory is completely invalid or had never existed at this point # If it previously existed but has since been deleted, you will get the revision where it was # deleted - so we need to handle that case separately later if newbranch: logger.info("Skipping update of layer %s for branch %s - subdirectory %s does not exist on this branch" % (layer.name, branchdesc, layerbranch.vcs_subdir)) elif layerbranch.vcs_subdir: logger.error("Subdirectory for layer %s does not exist on branch %s - if this is legitimate, the layer branch record should be deleted" % (layer.name, branchdesc)) else: logger.error("Failed to get last revision for layer %s on branch %s" % (layer.name, branchdesc)) transaction.rollback() continue layerdir = os.path.join(repodir, layerbranch.vcs_subdir) layerdir_start = os.path.normpath(layerdir) + os.sep layerrecipes = Recipe.objects.filter(layerbranch=layerbranch) layermachines = Machine.objects.filter(layerbranch=layerbranch) layerappends = BBAppend.objects.filter(layerbranch=layerbranch) layerclasses = BBClass.objects.filter(layerbranch=layerbranch) if layerbranch.vcs_last_rev != topcommit.hexsha or options.reload: # Check out appropriate branch if not options.nocheckout: out = utils.runcmd("git checkout origin/%s" % branchname, repodir, logger=logger) out = utils.runcmd("git clean -f -x", repodir, logger=logger) if layerbranch.vcs_subdir and not os.path.exists(layerdir): if newbranch: logger.info("Skipping update of layer %s for branch %s - subdirectory %s does not exist on this branch" % (layer.name, branchdesc, layerbranch.vcs_subdir)) else: logger.error("Subdirectory for layer %s does not exist on branch %s - if this is legitimate, the layer branch record should be deleted" % (layer.name, branchdesc)) transaction.rollback() continue if not os.path.exists(os.path.join(layerdir, 'conf/layer.conf')): logger.error("conf/layer.conf not found for layer %s - is subdirectory set correctly?" % layer.name) transaction.rollback() continue logger.info("Collecting data for layer %s on branch %s" % (layer.name, branchdesc)) try: config_data_copy = recipeparse.setup_layer(tinfoil.config_data, fetchdir, layerdir, layer, layerbranch) except recipeparse.RecipeParseError as e: logger.error(str(e)) transaction.rollback() continue if layerbranch.vcs_last_rev and not options.reload: try: diff = repo.commit(layerbranch.vcs_last_rev).diff(topcommit) except Exception as e: logger.warn("Unable to get diff from last commit hash for layer %s - falling back to slow update: %s" % (layer.name, str(e))) diff = None else: diff = None # We handle recipes specially to try to preserve the same id # when recipe upgrades happen (so that if a user bookmarks a # recipe page it remains valid) layerrecipes_delete = [] layerrecipes_add = [] # Check if any paths should be ignored because there are layers within this layer removedirs = [] for root, dirs, files in os.walk(layerdir): for d in dirs: if os.path.exists(os.path.join(root, d, 'conf', 'layer.conf')): removedirs.append(os.path.join(root, d) + os.sep) if diff: # Apply git changes to existing recipe list if layerbranch.vcs_subdir: subdir_start = os.path.normpath(layerbranch.vcs_subdir) + os.sep else: subdir_start = "" updatedrecipes = set() for d in diff.iter_change_type('D'): path = d.a_blob.path if path.startswith(subdir_start): skip = False for d in removedirs: if path.startswith(d): skip = True break if skip: continue (typename, filepath, filename) = recipeparse.detect_file_type(path, subdir_start) if typename == 'recipe': values = layerrecipes.filter(filepath=filepath).filter(filename=filename).values('id', 'filepath', 'filename', 'pn') if len(values): layerrecipes_delete.append(values[0]) logger.debug("Mark %s for deletion" % values[0]) updatedrecipes.add(os.path.join(values[0]['filepath'], values[0]['filename'])) else: logger.warn("Deleted recipe %s could not be found" % path) elif typename == 'bbappend': layerappends.filter(filepath=filepath).filter(filename=filename).delete() elif typename == 'machine': layermachines.filter(name=filename).delete() elif typename == 'bbclass': layerclasses.filter(name=filename).delete() for d in diff.iter_change_type('A'): path = d.b_blob.path if path.startswith(subdir_start): skip = False for d in removedirs: if path.startswith(d): skip = True break if skip: continue (typename, filepath, filename) = recipeparse.detect_file_type(path, subdir_start) if typename == 'recipe': layerrecipes_add.append(os.path.join(repodir, path)) logger.debug("Mark %s for addition" % path) updatedrecipes.add(os.path.join(filepath, filename)) elif typename == 'bbappend': append = BBAppend() append.layerbranch = layerbranch append.filename = filename append.filepath = filepath append.save() elif typename == 'machine': machine = Machine() machine.layerbranch = layerbranch machine.name = filename update_machine_conf_file(os.path.join(repodir, path), machine) machine.save() elif typename == 'bbclass': bbclass = BBClass() bbclass.layerbranch = layerbranch bbclass.name = filename bbclass.save() dirtyrecipes = set() for d in diff.iter_change_type('M'): path = d.a_blob.path if path.startswith(subdir_start): skip = False for d in removedirs: if path.startswith(d): skip = True break if skip: continue (typename, filepath, filename) = recipeparse.detect_file_type(path, subdir_start) if typename == 'recipe': logger.debug("Mark %s for update" % path) results = layerrecipes.filter(filepath=filepath).filter(filename=filename)[:1] if results: recipe = results[0] update_recipe_file(config_data_copy, os.path.join(layerdir, filepath), recipe, layerdir_start, repodir) recipe.save() updatedrecipes.add(recipe.full_path()) elif typename == 'machine': results = layermachines.filter(name=filename) if results: machine = results[0] update_machine_conf_file(os.path.join(repodir, path), machine) machine.save() deps = RecipeFileDependency.objects.filter(layerbranch=layerbranch).filter(path=path) for dep in deps: dirtyrecipes.add(dep.recipe) for recipe in dirtyrecipes: if not recipe.full_path() in updatedrecipes: update_recipe_file(config_data_copy, os.path.join(layerdir, recipe.filepath), recipe, layerdir_start, repodir) else: # Collect recipe data from scratch layerrecipe_fns = [] if options.fullreload: layerrecipes.delete() else: # First, check which recipes still exist layerrecipe_values = layerrecipes.values('id', 'filepath', 'filename', 'pn') for v in layerrecipe_values: root = os.path.join(layerdir, v['filepath']) fullpath = os.path.join(root, v['filename']) preserve = True if os.path.exists(fullpath): for d in removedirs: if fullpath.startswith(d): preserve = False break else: preserve = False if preserve: # Recipe still exists, update it results = layerrecipes.filter(id=v['id'])[:1] recipe = results[0] update_recipe_file(config_data_copy, root, recipe, layerdir_start, repodir) else: # Recipe no longer exists, mark it for later on layerrecipes_delete.append(v) layerrecipe_fns.append(fullpath) layermachines.delete() layerappends.delete() layerclasses.delete() for root, dirs, files in os.walk(layerdir): if '.git' in dirs: dirs.remove('.git') for d in dirs[:]: fullpath = os.path.join(root, d) + os.sep if fullpath in removedirs: dirs.remove(d) for f in files: fullpath = os.path.join(root, f) (typename, _, filename) = recipeparse.detect_file_type(fullpath, layerdir_start) if typename == 'recipe': if fullpath not in layerrecipe_fns: layerrecipes_add.append(fullpath) elif typename == 'bbappend': append = BBAppend() append.layerbranch = layerbranch append.filename = f append.filepath = os.path.relpath(root, layerdir) append.save() elif typename == 'machine': machine = Machine() machine.layerbranch = layerbranch machine.name = filename update_machine_conf_file(fullpath, machine) machine.save() elif typename == 'bbclass': bbclass = BBClass() bbclass.layerbranch = layerbranch bbclass.name = filename bbclass.save() for added in layerrecipes_add: # This is good enough without actually parsing the file (pn, pv) = recipeparse.split_recipe_fn(added) oldid = -1 for deleted in layerrecipes_delete: if deleted['pn'] == pn: oldid = deleted['id'] layerrecipes_delete.remove(deleted) break if oldid > -1: # Reclaim a record we would have deleted results = Recipe.objects.filter(id=oldid)[:1] recipe = results[0] logger.debug("Reclaim %s for %s %s" % (recipe, pn, pv)) else: # Create new record logger.debug("Add new recipe %s" % added) recipe = Recipe() recipe.layerbranch = layerbranch recipe.filename = os.path.basename(added) root = os.path.dirname(added) recipe.filepath = os.path.relpath(root, layerdir) update_recipe_file(config_data_copy, root, recipe, layerdir_start, repodir) recipe.save() for deleted in layerrecipes_delete: logger.debug("Delete %s" % deleted) results = Recipe.objects.filter(id=deleted['id'])[:1] recipe = results[0] recipe.delete() # Save repo info layerbranch.vcs_last_rev = topcommit.hexsha layerbranch.vcs_last_commit = datetime.fromtimestamp(topcommit.committed_date) else: logger.info("Layer %s is already up-to-date for branch %s" % (layer.name, branchdesc)) layerbranch.vcs_last_fetch = datetime.now() layerbranch.save() if options.dryrun: transaction.rollback() else: transaction.commit() except KeyboardInterrupt: transaction.rollback() logger.warn("Update interrupted, changes to %s rolled back" % layer.name) break except: import traceback traceback.print_exc() transaction.rollback() finally: transaction.leave_transaction_management() def check_machine_conf(path, subdir_start): subpath = path[len(subdir_start):] res = conf_re.match(subpath) if res: return res.group(1) return None def update_recipe_file(data, path, recipe, layerdir_start, repodir): fn = str(os.path.join(path, recipe.filename)) try: logger.debug('Updating recipe %s' % fn) envdata = bb.cache.Cache.loadDataFull(fn, [], data) envdata.setVar('SRCPV', 'X') recipe.pn = envdata.getVar("PN", True) recipe.pv = envdata.getVar("PV", True) recipe.summary = envdata.getVar("SUMMARY", True) recipe.description = envdata.getVar("DESCRIPTION", True) recipe.section = envdata.getVar("SECTION", True) recipe.license = envdata.getVar("LICENSE", True) recipe.homepage = envdata.getVar("HOMEPAGE", True) recipe.bugtracker = envdata.getVar("BUGTRACKER", True) or "" recipe.provides = envdata.getVar("PROVIDES", True) or "" recipe.bbclassextend = envdata.getVar("BBCLASSEXTEND", True) or "" src_uri = envdata.getVar("SRC_URI", True) or "" if (src_uri != ""): recipe.src_uri = src_uri.split()[0] recipe.depends = envdata.getVar("DEPENDS", True) or "" recipe.save() # Get file dependencies within this layer deps = envdata.getVar('__depends', True) filedeps = [] for depstr, date in deps: found = False if depstr.startswith(layerdir_start) and not depstr.endswith('/conf/layer.conf'): filedeps.append(os.path.relpath(depstr, repodir)) from layerindex.models import RecipeFileDependency RecipeFileDependency.objects.filter(recipe=recipe).delete() for filedep in filedeps: recipedep = RecipeFileDependency() recipedep.layerbranch = recipe.layerbranch recipedep.recipe = recipe recipedep.path = filedep recipedep.save() except KeyboardInterrupt: raise except BaseException as e: if not recipe.pn: recipe.pn = recipe.filename[:-3].split('_')[0] logger.error("Unable to read %s: %s", fn, str(e)) def update_machine_conf_file(path, machine): logger.debug('Updating machine %s' % path) desc = "" with open(path, 'r') as f: for line in f: if line.startswith('#@NAME:'): desc = line[7:].strip() if line.startswith('#@DESCRIPTION:'): desc = line[14:].strip() desc = re.sub(r'Machine configuration for( running)*( an)*( the)*', '', desc) break machine.description = desc