poky/scripts/lib/devtool/standard.py
Paul Eggleton efedd4323b devtool: update-recipe: add handling for git recipes
When updating git-based recipes, in a lot of cases what you want is to
push the changes to the repository and update SRCREV rather than to
apply patches within the recipe. Updating SRCREV is now the default
behaviour for recipes that fetch from git, but this can be overridden
in both directions using a new -m/--mode option.

(From OE-Core rev: 654792bb87610ee3569d02a85fa9ec071bf8ab6d)

Signed-off-by: Paul Eggleton <paul.eggleton@linux.intel.com>
Signed-off-by: Richard Purdie <richard.purdie@linuxfoundation.org>
2015-02-23 17:35:29 +00:00

625 lines
26 KiB
Python

# Development tool - standard commands plugin
#
# Copyright (C) 2014 Intel Corporation
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 as
# published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
import os
import sys
import re
import shutil
import glob
import tempfile
import logging
import argparse
from devtool import exec_build_env_command, setup_tinfoil
logger = logging.getLogger('devtool')
def plugin_init(pluginlist):
pass
def add(args, config, basepath, workspace):
import bb
import oe.recipeutils
if args.recipename in workspace:
logger.error("recipe %s is already in your workspace" % args.recipename)
return -1
reason = oe.recipeutils.validate_pn(args.recipename)
if reason:
logger.error(reason)
return -1
srctree = os.path.abspath(args.srctree)
appendpath = os.path.join(config.workspace_path, 'appends')
if not os.path.exists(appendpath):
os.makedirs(appendpath)
recipedir = os.path.join(config.workspace_path, 'recipes', args.recipename)
bb.utils.mkdirhier(recipedir)
if args.version:
if '_' in args.version or ' ' in args.version:
logger.error('Invalid version string "%s"' % args.version)
return -1
bp = "%s_%s" % (args.recipename, args.version)
else:
bp = args.recipename
recipefile = os.path.join(recipedir, "%s.bb" % bp)
if sys.stdout.isatty():
color = 'always'
else:
color = args.color
stdout, stderr = exec_build_env_command(config.init_path, basepath, 'recipetool --color=%s create -o %s %s' % (color, recipefile, srctree))
logger.info('Recipe %s has been automatically created; further editing may be required to make it fully functional' % recipefile)
_add_md5(config, args.recipename, recipefile)
initial_rev = None
if os.path.exists(os.path.join(srctree, '.git')):
(stdout, _) = bb.process.run('git rev-parse HEAD', cwd=srctree)
initial_rev = stdout.rstrip()
appendfile = os.path.join(appendpath, '%s.bbappend' % args.recipename)
with open(appendfile, 'w') as f:
f.write('inherit externalsrc\n')
f.write('EXTERNALSRC = "%s"\n' % srctree)
if args.same_dir:
f.write('EXTERNALSRC_BUILD = "%s"\n' % srctree)
if initial_rev:
f.write('\n# initial_rev: %s\n' % initial_rev)
_add_md5(config, args.recipename, appendfile)
return 0
def _get_recipe_file(cooker, pn):
import oe.recipeutils
recipefile = oe.recipeutils.pn_to_recipe(cooker, pn)
if not recipefile:
skipreasons = oe.recipeutils.get_unavailable_reasons(cooker, pn)
if skipreasons:
logger.error('\n'.join(skipreasons))
else:
logger.error("Unable to find any recipe file matching %s" % pn)
return recipefile
def extract(args, config, basepath, workspace):
import bb
import oe.recipeutils
tinfoil = setup_tinfoil()
recipefile = _get_recipe_file(tinfoil.cooker, args.recipename)
if not recipefile:
# Error already logged
return -1
rd = oe.recipeutils.parse_recipe(recipefile, tinfoil.config_data)
srctree = os.path.abspath(args.srctree)
initial_rev = _extract_source(srctree, args.keep_temp, args.branch, rd)
if initial_rev:
return 0
else:
return -1
def _extract_source(srctree, keep_temp, devbranch, d):
import bb.event
def eventfilter(name, handler, event, d):
if name == 'base_eventhandler':
return True
else:
return False
if hasattr(bb.event, 'set_eventfilter'):
bb.event.set_eventfilter(eventfilter)
pn = d.getVar('PN', True)
if pn == 'perf':
logger.error("The perf recipe does not actually check out source and thus cannot be supported by this tool")
return None
if 'work-shared' in d.getVar('S', True):
logger.error("The %s recipe uses a shared workdir which this tool does not currently support" % pn)
return None
if bb.data.inherits_class('externalsrc', d) and d.getVar('EXTERNALSRC', True):
logger.error("externalsrc is currently enabled for the %s recipe. This prevents the normal do_patch task from working. You will need to disable this first." % pn)
return None
if os.path.exists(srctree):
if not os.path.isdir(srctree):
logger.error("output path %s exists and is not a directory" % srctree)
return None
elif os.listdir(srctree):
logger.error("output path %s already exists and is non-empty" % srctree)
return None
# Prepare for shutil.move later on
bb.utils.mkdirhier(srctree)
os.rmdir(srctree)
initial_rev = None
tempdir = tempfile.mkdtemp(prefix='devtool')
try:
crd = d.createCopy()
# Make a subdir so we guard against WORKDIR==S
workdir = os.path.join(tempdir, 'workdir')
crd.setVar('WORKDIR', workdir)
crd.setVar('T', os.path.join(tempdir, 'temp'))
# FIXME: This is very awkward. Unfortunately it's not currently easy to properly
# execute tasks outside of bitbake itself, until then this has to suffice if we
# are to handle e.g. linux-yocto's extra tasks
executed = []
def exec_task_func(func, report):
if not func in executed:
deps = crd.getVarFlag(func, 'deps')
if deps:
for taskdepfunc in deps:
exec_task_func(taskdepfunc, True)
if report:
logger.info('Executing %s...' % func)
fn = d.getVar('FILE', True)
localdata = bb.build._task_data(fn, func, crd)
bb.build.exec_func(func, localdata)
executed.append(func)
logger.info('Fetching %s...' % pn)
exec_task_func('do_fetch', False)
logger.info('Unpacking...')
exec_task_func('do_unpack', False)
srcsubdir = crd.getVar('S', True)
if srcsubdir != workdir and os.path.dirname(srcsubdir) != workdir:
# Handle if S is set to a subdirectory of the source
srcsubdir = os.path.join(workdir, os.path.relpath(srcsubdir, workdir).split(os.sep)[0])
patchdir = os.path.join(srcsubdir, 'patches')
haspatches = False
if os.path.exists(patchdir):
if os.listdir(patchdir):
haspatches = True
else:
os.rmdir(patchdir)
if not bb.data.inherits_class('kernel-yocto', d):
if not os.listdir(srcsubdir):
logger.error("no source unpacked to S, perhaps the %s recipe doesn't use any source?" % pn)
return None
if not os.path.exists(os.path.join(srcsubdir, '.git')):
bb.process.run('git init', cwd=srcsubdir)
bb.process.run('git add .', cwd=srcsubdir)
bb.process.run('git commit -q -m "Initial commit from upstream at version %s"' % crd.getVar('PV', True), cwd=srcsubdir)
(stdout, _) = bb.process.run('git rev-parse HEAD', cwd=srcsubdir)
initial_rev = stdout.rstrip()
bb.process.run('git checkout -b %s' % devbranch, cwd=srcsubdir)
bb.process.run('git tag -f devtool-base', cwd=srcsubdir)
crd.setVar('PATCHTOOL', 'git')
logger.info('Patching...')
exec_task_func('do_patch', False)
bb.process.run('git tag -f devtool-patched', cwd=srcsubdir)
if os.path.exists(patchdir):
shutil.rmtree(patchdir)
if haspatches:
bb.process.run('git checkout patches', cwd=srcsubdir)
shutil.move(srcsubdir, srctree)
logger.info('Source tree extracted to %s' % srctree)
finally:
if keep_temp:
logger.info('Preserving temporary directory %s' % tempdir)
else:
shutil.rmtree(tempdir)
return initial_rev
def _add_md5(config, recipename, filename):
import bb.utils
md5 = bb.utils.md5_file(filename)
with open(os.path.join(config.workspace_path, '.devtool_md5'), 'a') as f:
f.write('%s|%s|%s\n' % (recipename, os.path.relpath(filename, config.workspace_path), md5))
def _check_preserve(config, recipename):
import bb.utils
origfile = os.path.join(config.workspace_path, '.devtool_md5')
newfile = os.path.join(config.workspace_path, '.devtool_md5_new')
preservepath = os.path.join(config.workspace_path, 'attic')
with open(origfile, 'r') as f:
with open(newfile, 'w') as tf:
for line in f.readlines():
splitline = line.rstrip().split('|')
if splitline[0] == recipename:
removefile = os.path.join(config.workspace_path, splitline[1])
md5 = bb.utils.md5_file(removefile)
if splitline[2] != md5:
bb.utils.mkdirhier(preservepath)
preservefile = os.path.basename(removefile)
logger.warn('File %s modified since it was written, preserving in %s' % (preservefile, preservepath))
shutil.move(removefile, os.path.join(preservepath, preservefile))
else:
os.remove(removefile)
else:
tf.write(line)
os.rename(newfile, origfile)
return False
def modify(args, config, basepath, workspace):
import bb
import oe.recipeutils
if args.recipename in workspace:
logger.error("recipe %s is already in your workspace" % args.recipename)
return -1
if not args.extract:
if not os.path.isdir(args.srctree):
logger.error("directory %s does not exist or not a directory (specify -x to extract source from recipe)" % args.srctree)
return -1
tinfoil = setup_tinfoil()
recipefile = _get_recipe_file(tinfoil.cooker, args.recipename)
if not recipefile:
# Error already logged
return -1
rd = oe.recipeutils.parse_recipe(recipefile, tinfoil.config_data)
initial_rev = None
commits = []
srctree = os.path.abspath(args.srctree)
if args.extract:
initial_rev = _extract_source(args.srctree, False, args.branch, rd)
if not initial_rev:
return -1
# Get list of commits since this revision
(stdout, _) = bb.process.run('git rev-list --reverse %s..HEAD' % initial_rev, cwd=args.srctree)
commits = stdout.split()
else:
if os.path.exists(os.path.join(args.srctree, '.git')):
(stdout, _) = bb.process.run('git rev-parse HEAD', cwd=args.srctree)
initial_rev = stdout.rstrip()
# Handle if S is set to a subdirectory of the source
s = rd.getVar('S', True)
workdir = rd.getVar('WORKDIR', True)
if s != workdir and os.path.dirname(s) != workdir:
srcsubdir = os.sep.join(os.path.relpath(s, workdir).split(os.sep)[1:])
srctree = os.path.join(srctree, srcsubdir)
appendpath = os.path.join(config.workspace_path, 'appends')
if not os.path.exists(appendpath):
os.makedirs(appendpath)
appendname = os.path.splitext(os.path.basename(recipefile))[0]
if args.wildcard:
appendname = re.sub(r'_.*', '_%', appendname)
appendfile = os.path.join(appendpath, appendname + '.bbappend')
with open(appendfile, 'w') as f:
f.write('FILESEXTRAPATHS_prepend := "${THISDIR}/${PN}:"\n\n')
f.write('inherit externalsrc\n')
f.write('# NOTE: We use pn- overrides here to avoid affecting multiple variants in the case where the recipe uses BBCLASSEXTEND\n')
f.write('EXTERNALSRC_pn-%s = "%s"\n' % (args.recipename, srctree))
if args.same_dir or bb.data.inherits_class('autotools-brokensep', rd):
if args.same_dir:
logger.info('using source tree as build directory since --same-dir specified')
else:
logger.info('using source tree as build directory since original recipe inherits autotools-brokensep')
f.write('EXTERNALSRC_BUILD_pn-%s = "%s"\n' % (args.recipename, srctree))
if initial_rev:
f.write('\n# initial_rev: %s\n' % initial_rev)
for commit in commits:
f.write('# commit: %s\n' % commit)
_add_md5(config, args.recipename, appendfile)
logger.info('Recipe %s now set up to build from %s' % (args.recipename, srctree))
return 0
def update_recipe(args, config, basepath, workspace):
if not args.recipename in workspace:
logger.error("no recipe named %s in your workspace" % args.recipename)
return -1
# Get initial revision from bbappend
appends = glob.glob(os.path.join(config.workspace_path, 'appends', '%s_*.bbappend' % args.recipename))
if not appends:
logger.error('unable to find workspace bbappend for recipe %s' % args.recipename)
return -1
tinfoil = setup_tinfoil()
import bb
from oe.patch import GitApplyTree
import oe.recipeutils
recipefile = _get_recipe_file(tinfoil.cooker, args.recipename)
if not recipefile:
# Error already logged
return -1
rd = oe.recipeutils.parse_recipe(recipefile, tinfoil.config_data)
orig_src_uri = rd.getVar('SRC_URI', False) or ''
if args.mode == 'auto':
if 'git://' in orig_src_uri:
mode = 'srcrev'
else:
mode = 'patch'
else:
mode = args.mode
def remove_patches(srcuri, patchlist):
# Remove any patches that we don't need
updated = False
for patch in patchlist:
patchfile = os.path.basename(patch)
for i in xrange(len(srcuri)):
if srcuri[i].startswith('file://') and os.path.basename(srcuri[i]).split(';')[0] == patchfile:
logger.info('Removing patch %s' % patchfile)
srcuri.pop(i)
# FIXME "git rm" here would be nice if the file in question is tracked
# FIXME there's a chance that this file is referred to by another recipe, in which case deleting wouldn't be the right thing to do
if patch.startswith(os.path.dirname(recipefile)):
os.remove(patch)
updated = True
break
return updated
srctree = workspace[args.recipename]
if mode == 'srcrev':
(stdout, _) = bb.process.run('git rev-parse HEAD', cwd=srctree)
srcrev = stdout.strip()
if len(srcrev) != 40:
logger.error('Invalid hash returned by git: %s' % stdout)
return 1
logger.info('Updating SRCREV in recipe %s' % os.path.basename(recipefile))
patchfields = {}
patchfields['SRCREV'] = srcrev
if not args.no_remove:
# Find list of existing patches in recipe file
existing_patches = oe.recipeutils.get_recipe_patches(rd)
old_srcrev = (rd.getVar('SRCREV', False) or '')
tempdir = tempfile.mkdtemp(prefix='devtool')
removepatches = []
try:
GitApplyTree.extractPatches(srctree, old_srcrev, tempdir)
newpatches = os.listdir(tempdir)
for patch in existing_patches:
patchfile = os.path.basename(patch)
if patchfile in newpatches:
removepatches.append(patch)
finally:
shutil.rmtree(tempdir)
if removepatches:
srcuri = (rd.getVar('SRC_URI', False) or '').split()
if remove_patches(srcuri, removepatches):
patchfields['SRC_URI'] = ' '.join(srcuri)
oe.recipeutils.patch_recipe(rd, recipefile, patchfields)
if not 'git://' in orig_src_uri:
logger.info('You will need to update SRC_URI within the recipe to point to a git repository where you have pushed your changes')
elif mode == 'patch':
commits = []
update_rev = None
if args.initial_rev:
initial_rev = args.initial_rev
else:
initial_rev = None
with open(appends[0], 'r') as f:
for line in f:
if line.startswith('# initial_rev:'):
initial_rev = line.split(':')[-1].strip()
elif line.startswith('# commit:'):
commits.append(line.split(':')[-1].strip())
if initial_rev:
# Find first actually changed revision
(stdout, _) = bb.process.run('git rev-list --reverse %s..HEAD' % initial_rev, cwd=srctree)
newcommits = stdout.split()
for i in xrange(min(len(commits), len(newcommits))):
if newcommits[i] == commits[i]:
update_rev = commits[i]
if not initial_rev:
logger.error('Unable to find initial revision - please specify it with --initial-rev')
return -1
if not update_rev:
update_rev = initial_rev
# Find list of existing patches in recipe file
existing_patches = oe.recipeutils.get_recipe_patches(rd)
removepatches = []
if not args.no_remove:
# Get all patches from source tree and check if any should be removed
tempdir = tempfile.mkdtemp(prefix='devtool')
try:
GitApplyTree.extractPatches(srctree, initial_rev, tempdir)
newpatches = os.listdir(tempdir)
for patch in existing_patches:
patchfile = os.path.basename(patch)
if patchfile not in newpatches:
removepatches.append(patch)
finally:
shutil.rmtree(tempdir)
# Get updated patches from source tree
tempdir = tempfile.mkdtemp(prefix='devtool')
try:
GitApplyTree.extractPatches(srctree, update_rev, tempdir)
# Match up and replace existing patches with corresponding new patches
updatepatches = False
updaterecipe = False
newpatches = os.listdir(tempdir)
for patch in existing_patches:
patchfile = os.path.basename(patch)
if patchfile in newpatches:
logger.info('Updating patch %s' % patchfile)
shutil.move(os.path.join(tempdir, patchfile), patch)
newpatches.remove(patchfile)
updatepatches = True
srcuri = (rd.getVar('SRC_URI', False) or '').split()
if newpatches:
# Add any patches left over
patchdir = os.path.join(os.path.dirname(recipefile), rd.getVar('BPN', True))
bb.utils.mkdirhier(patchdir)
for patchfile in newpatches:
logger.info('Adding new patch %s' % patchfile)
shutil.move(os.path.join(tempdir, patchfile), os.path.join(patchdir, patchfile))
srcuri.append('file://%s' % patchfile)
updaterecipe = True
if removepatches:
if remove_patches(srcuri, removepatches):
updaterecipe = True
if updaterecipe:
logger.info('Updating recipe %s' % os.path.basename(recipefile))
oe.recipeutils.patch_recipe(rd, recipefile, {'SRC_URI': ' '.join(srcuri)})
elif not updatepatches:
# Neither patches nor recipe were updated
logger.info('No patches need updating')
finally:
shutil.rmtree(tempdir)
else:
logger.error('update_recipe: invalid mode %s' % mode)
return 1
return 0
def status(args, config, basepath, workspace):
if workspace:
for recipe, value in workspace.iteritems():
print("%s: %s" % (recipe, value))
else:
logger.info('No recipes currently in your workspace - you can use "devtool modify" to work on an existing recipe or "devtool add" to add a new one')
return 0
def reset(args, config, basepath, workspace):
import bb.utils
if not args.recipename in workspace:
logger.error("no recipe named %s in your workspace" % args.recipename)
return -1
if not args.no_clean:
logger.info('Cleaning sysroot for recipe %s...' % args.recipename)
exec_build_env_command(config.init_path, basepath, 'bitbake -c clean %s' % args.recipename)
_check_preserve(config, args.recipename)
preservepath = os.path.join(config.workspace_path, 'attic', args.recipename)
def preservedir(origdir):
if os.path.exists(origdir):
for fn in os.listdir(origdir):
logger.warn('Preserving %s in %s' % (fn, preservepath))
bb.utils.mkdirhier(preservepath)
shutil.move(os.path.join(origdir, fn), os.path.join(preservepath, fn))
os.rmdir(origdir)
preservedir(os.path.join(config.workspace_path, 'recipes', args.recipename))
# We don't automatically create this dir next to appends, but the user can
preservedir(os.path.join(config.workspace_path, 'appends', args.recipename))
return 0
def build(args, config, basepath, workspace):
import bb
if not args.recipename in workspace:
logger.error("no recipe named %s in your workspace" % args.recipename)
return -1
build_task = config.get('Build', 'build_task', 'populate_sysroot')
exec_build_env_command(config.init_path, basepath, 'bitbake -c %s %s' % (build_task, args.recipename), watch=True)
return 0
def register_commands(subparsers, context):
parser_add = subparsers.add_parser('add', help='Add a new recipe',
description='Adds a new recipe',
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser_add.add_argument('recipename', help='Name for new recipe to add')
parser_add.add_argument('srctree', help='Path to external source tree')
parser_add.add_argument('--same-dir', '-s', help='Build in same directory as source', action="store_true")
parser_add.add_argument('--version', '-V', help='Version to use within recipe (PV)')
parser_add.set_defaults(func=add)
parser_add = subparsers.add_parser('modify', help='Modify the source for an existing recipe',
description='Enables modifying the source for an existing recipe',
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser_add.add_argument('recipename', help='Name for recipe to edit')
parser_add.add_argument('srctree', help='Path to external source tree')
parser_add.add_argument('--wildcard', '-w', action="store_true", help='Use wildcard for unversioned bbappend')
parser_add.add_argument('--extract', '-x', action="store_true", help='Extract source as well')
parser_add.add_argument('--same-dir', '-s', help='Build in same directory as source', action="store_true")
parser_add.add_argument('--branch', '-b', default="devtool", help='Name for development branch to checkout (only when using -x)')
parser_add.set_defaults(func=modify)
parser_add = subparsers.add_parser('extract', help='Extract the source for an existing recipe',
description='Extracts the source for an existing recipe',
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser_add.add_argument('recipename', help='Name for recipe to extract the source for')
parser_add.add_argument('srctree', help='Path to where to extract the source tree')
parser_add.add_argument('--branch', '-b', default="devtool", help='Name for development branch to checkout')
parser_add.add_argument('--keep-temp', action="store_true", help='Keep temporary directory (for debugging)')
parser_add.set_defaults(func=extract)
parser_add = subparsers.add_parser('update-recipe', help='Apply changes from external source tree to recipe',
description='Applies changes from external source tree to a recipe (updating/adding/removing patches as necessary, or by updating SRCREV)')
parser_add.add_argument('recipename', help='Name of recipe to update')
parser_add.add_argument('--mode', '-m', choices=['patch', 'srcrev', 'auto'], default='auto', help='Update mode (where %(metavar)s is %(choices)s; default is %(default)s)', metavar='MODE')
parser_add.add_argument('--initial-rev', help='Starting revision for patches')
parser_add.add_argument('--no-remove', '-n', action="store_true", help='Don\'t remove patches, only add or update')
parser_add.set_defaults(func=update_recipe)
parser_status = subparsers.add_parser('status', help='Show workspace status',
description='Lists recipes currently in your workspace and the paths to their respective external source trees',
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser_status.set_defaults(func=status)
parser_build = subparsers.add_parser('build', help='Build a recipe',
description='Builds the specified recipe using bitbake',
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser_build.add_argument('recipename', help='Recipe to build')
parser_build.set_defaults(func=build)
parser_reset = subparsers.add_parser('reset', help='Remove a recipe from your workspace',
description='Removes the specified recipe from your workspace (resetting its state)',
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser_reset.add_argument('recipename', help='Recipe to reset')
parser_reset.add_argument('--no-clean', '-n', action="store_true", help='Don\'t clean the sysroot to remove recipe output')
parser_reset.set_defaults(func=reset)