diff options
Diffstat (limited to 'scripts/lib/devtool')
| -rw-r--r-- | scripts/lib/devtool/__init__.py | 103 | ||||
| -rw-r--r-- | scripts/lib/devtool/build-image.py | 118 | ||||
| -rw-r--r-- | scripts/lib/devtool/build.py | 5 | ||||
| -rw-r--r-- | scripts/lib/devtool/build_image.py | 174 | ||||
| -rw-r--r-- | scripts/lib/devtool/build_sdk.py | 65 | ||||
| -rw-r--r-- | scripts/lib/devtool/deploy.py | 313 | ||||
| -rw-r--r-- | scripts/lib/devtool/package.py | 13 | ||||
| -rw-r--r-- | scripts/lib/devtool/runqemu.py | 20 | ||||
| -rw-r--r-- | scripts/lib/devtool/sdk.py | 215 | ||||
| -rw-r--r-- | scripts/lib/devtool/search.py | 7 | ||||
| -rw-r--r-- | scripts/lib/devtool/standard.py | 1347 | ||||
| -rw-r--r-- | scripts/lib/devtool/upgrade.py | 203 | ||||
| -rw-r--r-- | scripts/lib/devtool/utilcmds.py | 39 |
13 files changed, 1800 insertions, 822 deletions
diff --git a/scripts/lib/devtool/__init__.py b/scripts/lib/devtool/__init__.py index ff97dfc94b..d646b0cf63 100644 --- a/scripts/lib/devtool/__init__.py +++ b/scripts/lib/devtool/__init__.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # Development tool - utility functions for plugins # @@ -23,13 +23,15 @@ import sys import subprocess import logging import re +import codecs logger = logging.getLogger('devtool') - class DevtoolError(Exception): """Exception for handling devtool errors""" - pass + def __init__(self, message, exitcode=1): + super(DevtoolError, self).__init__(message) + self.exitcode = exitcode def exec_build_env_command(init_path, builddir, cmd, watch=False, **options): @@ -59,16 +61,17 @@ def exec_build_env_command(init_path, builddir, cmd, watch=False, **options): def exec_watch(cmd, **options): """Run program with stdout shown on sys.stdout""" import bb - if isinstance(cmd, basestring) and not "shell" in options: + if isinstance(cmd, str) and not "shell" in options: options["shell"] = True process = subprocess.Popen( cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, **options ) + reader = codecs.getreader('utf-8')(process.stdout) buf = '' while True: - out = process.stdout.read(1) + out = reader.read(1, 1) if out: sys.stdout.write(out) sys.stdout.flush() @@ -84,13 +87,13 @@ def exec_watch(cmd, **options): def exec_fakeroot(d, cmd, **kwargs): """Run a command under fakeroot (pseudo, in fact) so that it picks up the appropriate file permissions""" # Grab the command and check it actually exists - fakerootcmd = d.getVar('FAKEROOTCMD', True) + fakerootcmd = d.getVar('FAKEROOTCMD') if not os.path.exists(fakerootcmd): logger.error('pseudo executable %s could not be found - have you run a build yet? pseudo-native should install this and if you have run any build then that should have been built') return 2 # Set up the appropriate environment newenv = dict(os.environ) - fakerootenv = d.getVar('FAKEROOTENV', True) + fakerootenv = d.getVar('FAKEROOTENV') for varvalue in fakerootenv.split(): if '=' in varvalue: splitval = varvalue.split('=', 1) @@ -111,41 +114,40 @@ def setup_tinfoil(config_only=False, basepath=None, tracking=False): import bb.tinfoil tinfoil = bb.tinfoil.Tinfoil(tracking=tracking) - tinfoil.prepare(config_only) - tinfoil.logger.setLevel(logger.getEffectiveLevel()) + try: + tinfoil.prepare(config_only) + tinfoil.logger.setLevel(logger.getEffectiveLevel()) + except bb.tinfoil.TinfoilUIException: + tinfoil.shutdown() + raise DevtoolError('Failed to start bitbake environment') + except: + tinfoil.shutdown() + raise finally: os.chdir(orig_cwd) return tinfoil -def get_recipe_file(cooker, pn): - """Find recipe file corresponding a package name""" - 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 parse_recipe(config, tinfoil, pn, appends, filter_workspace=True): - """Parse recipe of a package""" - import oe.recipeutils - recipefile = get_recipe_file(tinfoil.cooker, pn) - if not recipefile: - # Error already logged + """Parse the specified recipe""" + try: + recipefile = tinfoil.get_recipe_file(pn) + except bb.providers.NoProvider as e: + logger.error(str(e)) return None if appends: - append_files = tinfoil.cooker.collection.get_file_appends(recipefile) + append_files = tinfoil.get_file_appends(recipefile) if filter_workspace: # Filter out appends from the workspace append_files = [path for path in append_files if not path.startswith(config.workspace_path)] else: append_files = None - return oe.recipeutils.parse_recipe(recipefile, append_files, - tinfoil.config_data) + try: + rd = tinfoil.parse_recipe_file(recipefile, appends, append_files) + except Exception as e: + logger.error(str(e)) + return None + return rd def check_workspace_recipe(workspace, pn, checksrc=True, bbclassextend=False): """ @@ -155,7 +157,7 @@ def check_workspace_recipe(workspace, pn, checksrc=True, bbclassextend=False): workspacepn = pn - for recipe, value in workspace.iteritems(): + for recipe, value in workspace.items(): if recipe == pn: break if bbclassextend: @@ -189,21 +191,24 @@ def use_external_build(same_dir, no_same_dir, d): logger.info('Using source tree as build directory since --same-dir specified') elif bb.data.inherits_class('autotools-brokensep', d): logger.info('Using source tree as build directory since recipe inherits autotools-brokensep') - elif d.getVar('B', True) == os.path.abspath(d.getVar('S', True)): + elif d.getVar('B') == os.path.abspath(d.getVar('S')): logger.info('Using source tree as build directory since that would be the default for this recipe') else: b_is_s = False return b_is_s -def setup_git_repo(repodir, version, devbranch, basetag='devtool-base'): +def setup_git_repo(repodir, version, devbranch, basetag='devtool-base', d=None): """ Set up the git repository for the source tree """ import bb.process + import oe.patch if not os.path.exists(os.path.join(repodir, '.git')): bb.process.run('git init', cwd=repodir) bb.process.run('git add .', cwd=repodir) - commit_cmd = ['git', 'commit', '-q'] + commit_cmd = ['git'] + oe.patch.GitApplyTree.gitCommandUserOptions(commit_cmd, d=d) + commit_cmd += ['commit', '-q'] stdout, _ = bb.process.run('git status --porcelain', cwd=repodir) if not stdout: commit_cmd.append('--allow-empty') @@ -255,3 +260,35 @@ def get_bbclassextend_targets(recipefile, pn): elif variant in ['native', 'cross', 'crosssdk']: targets.append('%s-%s' % (pn, variant)) return targets + +def ensure_npm(config, basepath, fixed_setup=False, check_exists=True): + """ + Ensure that npm is available and either build it or show a + reasonable error message + """ + if check_exists: + tinfoil = setup_tinfoil(config_only=False, basepath=basepath) + try: + rd = tinfoil.parse_recipe('nodejs-native') + nativepath = rd.getVar('STAGING_BINDIR_NATIVE') + finally: + tinfoil.shutdown() + npmpath = os.path.join(nativepath, 'npm') + build_npm = not os.path.exists(npmpath) + else: + build_npm = True + + if build_npm: + logger.info('Building nodejs-native') + try: + exec_build_env_command(config.init_path, basepath, + 'bitbake -q nodejs-native -c addto_recipe_sysroot', watch=True) + except bb.process.ExecutionError as e: + if "Nothing PROVIDES 'nodejs-native'" in e.stdout: + if fixed_setup: + msg = 'nodejs-native is required for npm but is not available within this SDK' + else: + msg = 'nodejs-native is required for npm but is not available - you will likely need to add a layer that provides nodejs' + raise DevtoolError(msg) + else: + raise diff --git a/scripts/lib/devtool/build-image.py b/scripts/lib/devtool/build-image.py deleted file mode 100644 index 48c3a1198a..0000000000 --- a/scripts/lib/devtool/build-image.py +++ /dev/null @@ -1,118 +0,0 @@ -# Development tool - build-image plugin -# -# Copyright (C) 2015 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. - -"""Devtool plugin containing the build-image subcommand.""" - -import os -import logging - -from bb.process import ExecutionError -from devtool import exec_build_env_command, setup_tinfoil, parse_recipe, DevtoolError - -logger = logging.getLogger('devtool') - -def _get_packages(tinfoil, workspace, config): - """Get list of packages from recipes in the workspace.""" - result = [] - for recipe in workspace: - data = parse_recipe(config, tinfoil, recipe, True) - if 'class-target' in data.getVar('OVERRIDES', True).split(':'): - if recipe in data.getVar('PACKAGES', True): - result.append(recipe) - else: - logger.warning("Skipping recipe %s as it doesn't produce a " - "package with the same name", recipe) - return result - -def build_image(args, config, basepath, workspace): - """Entry point for the devtool 'build-image' subcommand.""" - - image = args.imagename - auto_image = False - if not image: - sdk_targets = config.get('SDK', 'sdk_targets', '').split() - if sdk_targets: - image = sdk_targets[0] - auto_image = True - if not image: - raise DevtoolError('Unable to determine image to build, please specify one') - - appendfile = os.path.join(config.workspace_path, 'appends', - '%s.bbappend' % image) - - # remove <image>.bbappend to make sure setup_tinfoil doesn't - # break because of it - if os.path.isfile(appendfile): - os.unlink(appendfile) - - tinfoil = setup_tinfoil(basepath=basepath) - rd = parse_recipe(config, tinfoil, image, True) - if not rd: - # Error already shown - return 1 - if not bb.data.inherits_class('image', rd): - if auto_image: - raise DevtoolError('Unable to determine image to build, please specify one') - else: - raise DevtoolError('Specified recipe %s is not an image recipe' % image) - - try: - if workspace or args.add_packages: - if args.add_packages: - packages = args.add_packages.split(',') - else: - packages = _get_packages(tinfoil, workspace, config) - if packages: - with open(appendfile, 'w') as afile: - # include packages from workspace recipes into the image - afile.write('IMAGE_INSTALL_append = " %s"\n' % ' '.join(packages)) - logger.info('Building image %s with the following ' - 'additional packages: %s', image, ' '.join(packages)) - else: - logger.warning('No packages to add, building image %s unmodified', image) - else: - logger.warning('No recipes in workspace, building image %s unmodified', image) - - deploy_dir_image = tinfoil.config_data.getVar('DEPLOY_DIR_IMAGE', True) - - tinfoil.shutdown() - - # run bitbake to build image - try: - exec_build_env_command(config.init_path, basepath, - 'bitbake %s' % image, watch=True) - except ExecutionError as err: - return err.exitcode - finally: - if os.path.isfile(appendfile): - os.unlink(appendfile) - - logger.info('Successfully built %s. You can find output files in %s' - % (image, deploy_dir_image)) - -def register_commands(subparsers, context): - """Register devtool subcommands from the build-image plugin""" - parser = subparsers.add_parser('build-image', - help='Build image including workspace recipe packages', - description='Builds an image, extending it to include ' - 'packages from recipes in the workspace') - parser.add_argument('imagename', help='Image recipe to build', nargs='?') - parser.add_argument('-p', '--add-packages', help='Instead of adding packages for the ' - 'entire workspace, specify packages to be added to the image ' - '(separate multiple packages by commas)', - metavar='PACKAGES') - parser.set_defaults(func=build_image) diff --git a/scripts/lib/devtool/build.py b/scripts/lib/devtool/build.py index b10a6a903b..252379e9b2 100644 --- a/scripts/lib/devtool/build.py +++ b/scripts/lib/devtool/build.py @@ -27,7 +27,7 @@ logger = logging.getLogger('devtool') def _set_file_values(fn, values): - remaining = values.keys() + remaining = list(values.keys()) def varfunc(varname, origvalue, op, newlines): newvalue = values.get(varname, origvalue) @@ -79,7 +79,8 @@ def build(args, config, basepath, workspace): def register_commands(subparsers, context): """Register devtool subcommands from this plugin""" parser_build = subparsers.add_parser('build', help='Build a recipe', - description='Builds the specified recipe using bitbake (up to and including %s)' % ', '.join(_get_build_tasks(context.config))) + description='Builds the specified recipe using bitbake (up to and including %s)' % ', '.join(_get_build_tasks(context.config)), + group='working', order=50) parser_build.add_argument('recipename', help='Recipe to build') parser_build.add_argument('-s', '--disable-parallel-make', action="store_true", help='Disable make parallelism') parser_build.set_defaults(func=build) diff --git a/scripts/lib/devtool/build_image.py b/scripts/lib/devtool/build_image.py new file mode 100644 index 0000000000..e5810389be --- /dev/null +++ b/scripts/lib/devtool/build_image.py @@ -0,0 +1,174 @@ +# Development tool - build-image plugin +# +# Copyright (C) 2015 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. + +"""Devtool plugin containing the build-image subcommand.""" + +import os +import errno +import logging + +from bb.process import ExecutionError +from devtool import exec_build_env_command, setup_tinfoil, parse_recipe, DevtoolError + +logger = logging.getLogger('devtool') + +class TargetNotImageError(Exception): + pass + +def _get_packages(tinfoil, workspace, config): + """Get list of packages from recipes in the workspace.""" + result = [] + for recipe in workspace: + data = parse_recipe(config, tinfoil, recipe, True) + if 'class-target' in data.getVar('OVERRIDES').split(':'): + if recipe in data.getVar('PACKAGES').split(): + result.append(recipe) + else: + logger.warning("Skipping recipe %s as it doesn't produce a " + "package with the same name", recipe) + return result + +def build_image(args, config, basepath, workspace): + """Entry point for the devtool 'build-image' subcommand.""" + + image = args.imagename + auto_image = False + if not image: + sdk_targets = config.get('SDK', 'sdk_targets', '').split() + if sdk_targets: + image = sdk_targets[0] + auto_image = True + if not image: + raise DevtoolError('Unable to determine image to build, please specify one') + + try: + if args.add_packages: + add_packages = args.add_packages.split(',') + else: + add_packages = None + result, outputdir = build_image_task(config, basepath, workspace, image, add_packages) + except TargetNotImageError: + if auto_image: + raise DevtoolError('Unable to determine image to build, please specify one') + else: + raise DevtoolError('Specified recipe %s is not an image recipe' % image) + + if result == 0: + logger.info('Successfully built %s. You can find output files in %s' + % (image, outputdir)) + return result + +def build_image_task(config, basepath, workspace, image, add_packages=None, task=None, extra_append=None): + # remove <image>.bbappend to make sure setup_tinfoil doesn't + # break because of it + target_basename = config.get('SDK', 'target_basename', '') + if target_basename: + appendfile = os.path.join(config.workspace_path, 'appends', + '%s.bbappend' % target_basename) + try: + os.unlink(appendfile) + except OSError as exc: + if exc.errno != errno.ENOENT: + raise + + tinfoil = setup_tinfoil(basepath=basepath) + try: + rd = parse_recipe(config, tinfoil, image, True) + if not rd: + # Error already shown + return (1, None) + if not bb.data.inherits_class('image', rd): + raise TargetNotImageError() + + # Get the actual filename used and strip the .bb and full path + target_basename = rd.getVar('FILE') + target_basename = os.path.splitext(os.path.basename(target_basename))[0] + config.set('SDK', 'target_basename', target_basename) + config.write() + + appendfile = os.path.join(config.workspace_path, 'appends', + '%s.bbappend' % target_basename) + + outputdir = None + try: + if workspace or add_packages: + if add_packages: + packages = add_packages + else: + packages = _get_packages(tinfoil, workspace, config) + else: + packages = None + if not task: + if not packages and not add_packages and workspace: + logger.warning('No recipes in workspace, building image %s unmodified', image) + elif not packages: + logger.warning('No packages to add, building image %s unmodified', image) + + if packages or extra_append: + bb.utils.mkdirhier(os.path.dirname(appendfile)) + with open(appendfile, 'w') as afile: + if packages: + # include packages from workspace recipes into the image + afile.write('IMAGE_INSTALL_append = " %s"\n' % ' '.join(packages)) + if not task: + logger.info('Building image %s with the following ' + 'additional packages: %s', image, ' '.join(packages)) + if extra_append: + for line in extra_append: + afile.write('%s\n' % line) + + if task in ['populate_sdk', 'populate_sdk_ext']: + outputdir = rd.getVar('SDK_DEPLOY') + else: + outputdir = rd.getVar('DEPLOY_DIR_IMAGE') + + tmp_tinfoil = tinfoil + tinfoil = None + tmp_tinfoil.shutdown() + + options = '' + if task: + options += '-c %s' % task + + # run bitbake to build image (or specified task) + try: + exec_build_env_command(config.init_path, basepath, + 'bitbake %s %s' % (options, image), watch=True) + except ExecutionError as err: + return (err.exitcode, None) + finally: + if os.path.isfile(appendfile): + os.unlink(appendfile) + finally: + if tinfoil: + tinfoil.shutdown() + return (0, outputdir) + + +def register_commands(subparsers, context): + """Register devtool subcommands from the build-image plugin""" + parser = subparsers.add_parser('build-image', + help='Build image including workspace recipe packages', + description='Builds an image, extending it to include ' + 'packages from recipes in the workspace', + group='testbuild', order=-10) + parser.add_argument('imagename', help='Image recipe to build', nargs='?') + parser.add_argument('-p', '--add-packages', help='Instead of adding packages for the ' + 'entire workspace, specify packages to be added to the image ' + '(separate multiple packages by commas)', + metavar='PACKAGES') + parser.set_defaults(func=build_image) diff --git a/scripts/lib/devtool/build_sdk.py b/scripts/lib/devtool/build_sdk.py new file mode 100644 index 0000000000..b89d65b0cb --- /dev/null +++ b/scripts/lib/devtool/build_sdk.py @@ -0,0 +1,65 @@ +# Development tool - build-sdk command plugin +# +# Copyright (C) 2015-2016 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 subprocess +import logging +import glob +import shutil +import errno +import sys +import tempfile +from devtool import exec_build_env_command, setup_tinfoil, parse_recipe, DevtoolError +from devtool import build_image + +logger = logging.getLogger('devtool') + + +def build_sdk(args, config, basepath, workspace): + """Entry point for the devtool build-sdk command""" + + sdk_targets = config.get('SDK', 'sdk_targets', '').split() + if sdk_targets: + image = sdk_targets[0] + else: + raise DevtoolError('Unable to determine image to build SDK for') + + extra_append = ['SDK_DERIVATIVE = "1"'] + try: + result, outputdir = build_image.build_image_task(config, + basepath, + workspace, + image, + task='populate_sdk_ext', + extra_append=extra_append) + except build_image.TargetNotImageError: + raise DevtoolError('Unable to determine image to build SDK for') + + if result == 0: + logger.info('Successfully built SDK. You can find output files in %s' + % outputdir) + return result + + +def register_commands(subparsers, context): + """Register devtool subcommands""" + if context.fixed_setup: + parser_build_sdk = subparsers.add_parser('build-sdk', + help='Build a derivative SDK of this one', + description='Builds an extensible SDK based upon this one and the items in your workspace', + group='advanced') + parser_build_sdk.set_defaults(func=build_sdk) diff --git a/scripts/lib/devtool/deploy.py b/scripts/lib/devtool/deploy.py index c90c6b1f76..b3730ae833 100644 --- a/scripts/lib/devtool/deploy.py +++ b/scripts/lib/devtool/deploy.py @@ -1,6 +1,6 @@ # Development tool - deploy/undeploy command plugin # -# Copyright (C) 2014-2015 Intel Corporation +# Copyright (C) 2014-2016 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 @@ -19,13 +19,127 @@ import os import subprocess import logging +import tempfile +import shutil +import argparse_oe from devtool import exec_fakeroot, setup_tinfoil, check_workspace_recipe, DevtoolError logger = logging.getLogger('devtool') +deploylist_path = '/.devtool' + +def _prepare_remote_script(deploy, verbose=False, dryrun=False, undeployall=False, nopreserve=False, nocheckspace=False): + """ + Prepare a shell script for running on the target to + deploy/undeploy files. We have to be careful what we put in this + script - only commands that are likely to be available on the + target are suitable (the target might be constrained, e.g. using + busybox rather than bash with coreutils). + """ + lines = [] + lines.append('#!/bin/sh') + lines.append('set -e') + if undeployall: + # Yes, I know this is crude - but it does work + lines.append('for entry in %s/*.list; do' % deploylist_path) + lines.append('[ ! -f $entry ] && exit') + lines.append('set `basename $entry | sed "s/.list//"`') + if dryrun: + if not deploy: + lines.append('echo "Previously deployed files for $1:"') + lines.append('manifest="%s/$1.list"' % deploylist_path) + lines.append('preservedir="%s/$1.preserve"' % deploylist_path) + lines.append('if [ -f $manifest ] ; then') + # Read manifest in reverse and delete files / remove empty dirs + lines.append(' sed \'1!G;h;$!d\' $manifest | while read file') + lines.append(' do') + if dryrun: + lines.append(' if [ ! -d $file ] ; then') + lines.append(' echo $file') + lines.append(' fi') + else: + lines.append(' if [ -d $file ] ; then') + # Avoid deleting a preserved directory in case it has special perms + lines.append(' if [ ! -d $preservedir/$file ] ; then') + lines.append(' rmdir $file > /dev/null 2>&1 || true') + lines.append(' fi') + lines.append(' else') + lines.append(' rm $file') + lines.append(' fi') + lines.append(' done') + if not dryrun: + lines.append(' rm $manifest') + if not deploy and not dryrun: + # May as well remove all traces + lines.append(' rmdir `dirname $manifest` > /dev/null 2>&1 || true') + lines.append('fi') + + if deploy: + if not nocheckspace: + # Check for available space + # FIXME This doesn't take into account files spread across multiple + # partitions, but doing that is non-trivial + # Find the part of the destination path that exists + lines.append('checkpath="$2"') + lines.append('while [ "$checkpath" != "/" ] && [ ! -e $checkpath ]') + lines.append('do') + lines.append(' checkpath=`dirname "$checkpath"`') + lines.append('done') + lines.append(r'freespace=$(df -P $checkpath | sed -nre "s/^(\S+\s+){3}([0-9]+).*/\2/p")') + # First line of the file is the total space + lines.append('total=`head -n1 $3`') + lines.append('if [ $total -gt $freespace ] ; then') + lines.append(' echo "ERROR: insufficient space on target (available ${freespace}, needed ${total})"') + lines.append(' exit 1') + lines.append('fi') + if not nopreserve: + # Preserve any files that exist. Note that this will add to the + # preserved list with successive deployments if the list of files + # deployed changes, but because we've deleted any previously + # deployed files at this point it will never preserve anything + # that was deployed, only files that existed prior to any deploying + # (which makes the most sense) + lines.append('cat $3 | sed "1d" | while read file fsize') + lines.append('do') + lines.append(' if [ -e $file ] ; then') + lines.append(' dest="$preservedir/$file"') + lines.append(' mkdir -p `dirname $dest`') + lines.append(' mv $file $dest') + lines.append(' fi') + lines.append('done') + lines.append('rm $3') + lines.append('mkdir -p `dirname $manifest`') + lines.append('mkdir -p $2') + if verbose: + lines.append(' tar xv -C $2 -f - | tee $manifest') + else: + lines.append(' tar xv -C $2 -f - > $manifest') + lines.append('sed -i "s!^./!$2!" $manifest') + elif not dryrun: + # Put any preserved files back + lines.append('if [ -d $preservedir ] ; then') + lines.append(' cd $preservedir') + lines.append(' find . -type f -exec mv {} /{} \;') + lines.append(' cd /') + lines.append(' rm -rf $preservedir') + lines.append('fi') + + if undeployall: + if not dryrun: + lines.append('echo "NOTE: Successfully undeployed $1"') + lines.append('done') + + # Delete the script itself + lines.append('rm $0') + lines.append('') + + return '\n'.join(lines) + + def deploy(args, config, basepath, workspace): """Entry point for the devtool 'deploy' subcommand""" import re + import math import oe.recipeutils check_workspace_recipe(workspace, args.recipename, checksrc=False) @@ -36,76 +150,108 @@ def deploy(args, config, basepath, workspace): destdir = '/' else: args.target = host - - deploy_dir = os.path.join(basepath, 'target_deploy', args.target) - deploy_file = os.path.join(deploy_dir, args.recipename + '.list') + if not destdir.endswith('/'): + destdir += '/' tinfoil = setup_tinfoil(basepath=basepath) try: - rd = oe.recipeutils.parse_recipe_simple(tinfoil.cooker, args.recipename, tinfoil.config_data) - except Exception as e: - raise DevtoolError('Exception parsing recipe %s: %s' % - (args.recipename, e)) - recipe_outdir = rd.getVar('D', True) - if not os.path.exists(recipe_outdir) or not os.listdir(recipe_outdir): - raise DevtoolError('No files to deploy - have you built the %s ' - 'recipe? If so, the install step has not installed ' - 'any files.' % args.recipename) - - if args.dry_run: - print('Files to be deployed for %s on target %s:' % (args.recipename, args.target)) + try: + rd = tinfoil.parse_recipe(args.recipename) + except Exception as e: + raise DevtoolError('Exception parsing recipe %s: %s' % + (args.recipename, e)) + recipe_outdir = rd.getVar('D') + if not os.path.exists(recipe_outdir) or not os.listdir(recipe_outdir): + raise DevtoolError('No files to deploy - have you built the %s ' + 'recipe? If so, the install step has not installed ' + 'any files.' % args.recipename) + + filelist = [] + ftotalsize = 0 for root, _, files in os.walk(recipe_outdir): for fn in files: - print(' %s' % os.path.join(destdir, os.path.relpath(root, recipe_outdir), fn)) - return 0 + # Get the size in kiB (since we'll be comparing it to the output of du -k) + # MUST use lstat() here not stat() or getfilesize() since we don't want to + # dereference symlinks + fsize = int(math.ceil(float(os.lstat(os.path.join(root, fn)).st_size)/1024)) + ftotalsize += fsize + # The path as it would appear on the target + fpath = os.path.join(destdir, os.path.relpath(root, recipe_outdir), fn) + filelist.append((fpath, fsize)) - if os.path.exists(deploy_file): - if undeploy(args, config, basepath, workspace): - # Error already shown - return 1 + if args.dry_run: + print('Files to be deployed for %s on target %s:' % (args.recipename, args.target)) + for item, _ in filelist: + print(' %s' % item) + return 0 - extraoptions = '' - if args.no_host_check: - extraoptions += '-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no' - if args.show_status: - tarextractopts = 'xv' - else: - tarextractopts = 'x' - extraoptions += ' -q' - # We cannot use scp here, because it doesn't preserve symlinks - ret = exec_fakeroot(rd, 'tar cf - . | ssh %s %s \'tar %s -C %s -f -\'' % (extraoptions, args.target, tarextractopts, destdir), cwd=recipe_outdir, shell=True) - if ret != 0: - raise DevtoolError('Deploy failed - rerun with -s to get a complete ' - 'error message') - logger.info('Successfully deployed %s' % recipe_outdir) + extraoptions = '' + if args.no_host_check: + extraoptions += '-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no' + if not args.show_status: + extraoptions += ' -q' + + scp_port = '' + ssh_port = '' + if not args.port: + raise DevtoolError("If you specify -P/--port then you must provide the port to be used to connect to the target") + else: + scp_port = "-P %s" % args.port + ssh_port = "-p %s" % args.port - if not os.path.exists(deploy_dir): - os.makedirs(deploy_dir) + # In order to delete previously deployed files and have the manifest file on + # the target, we write out a shell script and then copy it to the target + # so we can then run it (piping tar output to it). + # (We cannot use scp here, because it doesn't preserve symlinks.) + tmpdir = tempfile.mkdtemp(prefix='devtool') + try: + tmpscript = '/tmp/devtool_deploy.sh' + tmpfilelist = os.path.join(os.path.dirname(tmpscript), 'devtool_deploy.list') + shellscript = _prepare_remote_script(deploy=True, + verbose=args.show_status, + nopreserve=args.no_preserve, + nocheckspace=args.no_check_space) + # Write out the script to a file + with open(os.path.join(tmpdir, os.path.basename(tmpscript)), 'w') as f: + f.write(shellscript) + # Write out the file list + with open(os.path.join(tmpdir, os.path.basename(tmpfilelist)), 'w') as f: + f.write('%d\n' % ftotalsize) + for fpath, fsize in filelist: + f.write('%s %d\n' % (fpath, fsize)) + # Copy them to the target + ret = subprocess.call("scp %s %s %s/* %s:%s" % (scp_port, extraoptions, tmpdir, args.target, os.path.dirname(tmpscript)), shell=True) + if ret != 0: + raise DevtoolError('Failed to copy script to %s - rerun with -s to ' + 'get a complete error message' % args.target) + finally: + shutil.rmtree(tmpdir) - files_list = [] - for root, _, files in os.walk(recipe_outdir): - for filename in files: - filename = os.path.relpath(os.path.join(root, filename), recipe_outdir) - files_list.append(os.path.join(destdir, filename)) + # Now run the script + ret = exec_fakeroot(rd, 'tar cf - . | ssh %s %s %s \'sh %s %s %s %s\'' % (ssh_port, extraoptions, args.target, tmpscript, args.recipename, destdir, tmpfilelist), cwd=recipe_outdir, shell=True) + if ret != 0: + raise DevtoolError('Deploy failed - rerun with -s to get a complete ' + 'error message') - with open(deploy_file, 'w') as fobj: - fobj.write('\n'.join(files_list)) + logger.info('Successfully deployed %s' % recipe_outdir) + + files_list = [] + for root, _, files in os.walk(recipe_outdir): + for filename in files: + filename = os.path.relpath(os.path.join(root, filename), recipe_outdir) + files_list.append(os.path.join(destdir, filename)) + finally: + tinfoil.shutdown() return 0 def undeploy(args, config, basepath, workspace): """Entry point for the devtool 'undeploy' subcommand""" - deploy_file = os.path.join(basepath, 'target_deploy', args.target, args.recipename + '.list') - if not os.path.exists(deploy_file): - raise DevtoolError('%s has not been deployed' % args.recipename) - - if args.dry_run: - print('Previously deployed files to be un-deployed for %s on target %s:' % (args.recipename, args.target)) - with open(deploy_file, 'r') as f: - for line in f: - print(' %s' % line.rstrip()) - return 0 + if args.all and args.recipename: + raise argparse_oe.ArgumentUsageError('Cannot specify -a/--all with a recipe name', 'undeploy-target') + elif not args.recipename and not args.all: + raise argparse_oe.ArgumentUsageError('If you don\'t specify a recipe, you must specify -a/--all', 'undeploy-target') extraoptions = '' if args.no_host_check: @@ -113,36 +259,67 @@ def undeploy(args, config, basepath, workspace): if not args.show_status: extraoptions += ' -q' - ret = subprocess.call("scp %s %s %s:/tmp" % (extraoptions, deploy_file, args.target), shell=True) - if ret != 0: - raise DevtoolError('Failed to copy file list to %s - rerun with -s to ' - 'get a complete error message' % args.target) - - ret = subprocess.call("ssh %s %s 'xargs -n1 rm -f </tmp/%s'" % (extraoptions, args.target, os.path.basename(deploy_file)), shell=True) - if ret == 0: - logger.info('Successfully undeployed %s' % args.recipename) - os.remove(deploy_file) + scp_port = '' + ssh_port = '' + if not args.port: + raise DevtoolError("If you specify -P/--port then you must provide the port to be used to connect to the target") else: + scp_port = "-P %s" % args.port + ssh_port = "-p %s" % args.port + + args.target = args.target.split(':')[0] + + tmpdir = tempfile.mkdtemp(prefix='devtool') + try: + tmpscript = '/tmp/devtool_undeploy.sh' + shellscript = _prepare_remote_script(deploy=False, dryrun=args.dry_run, undeployall=args.all) + # Write out the script to a file + with open(os.path.join(tmpdir, os.path.basename(tmpscript)), 'w') as f: + f.write(shellscript) + # Copy it to the target + ret = subprocess.call("scp %s %s %s/* %s:%s" % (scp_port, extraoptions, tmpdir, args.target, os.path.dirname(tmpscript)), shell=True) + if ret != 0: + raise DevtoolError('Failed to copy script to %s - rerun with -s to ' + 'get a complete error message' % args.target) + finally: + shutil.rmtree(tmpdir) + + # Now run the script + ret = subprocess.call('ssh %s %s %s \'sh %s %s\'' % (ssh_port, extraoptions, args.target, tmpscript, args.recipename), shell=True) + if ret != 0: raise DevtoolError('Undeploy failed - rerun with -s to get a complete ' 'error message') - return ret + if not args.all and not args.dry_run: + logger.info('Successfully undeployed %s' % args.recipename) + return 0 def register_commands(subparsers, context): """Register devtool subcommands from the deploy plugin""" - parser_deploy = subparsers.add_parser('deploy-target', help='Deploy recipe output files to live target machine') + parser_deploy = subparsers.add_parser('deploy-target', + help='Deploy recipe output files to live target machine', + description='Deploys a recipe\'s build output (i.e. the output of the do_install task) to a live target machine over ssh. By default, any existing files will be preserved instead of being overwritten and will be restored if you run devtool undeploy-target. Note: this only deploys the recipe itself and not any runtime dependencies, so it is assumed that those have been installed on the target beforehand.', + group='testbuild') parser_deploy.add_argument('recipename', help='Recipe to deploy') parser_deploy.add_argument('target', help='Live target machine running an ssh server: user@hostname[:destdir]') parser_deploy.add_argument('-c', '--no-host-check', help='Disable ssh host key checking', action='store_true') parser_deploy.add_argument('-s', '--show-status', help='Show progress/status output', action='store_true') parser_deploy.add_argument('-n', '--dry-run', help='List files to be deployed only', action='store_true') + parser_deploy.add_argument('-p', '--no-preserve', help='Do not preserve existing files', action='store_true') + parser_deploy.add_argument('--no-check-space', help='Do not check for available space before deploying', action='store_true') + parser_deploy.add_argument('-P', '--port', default='22', help='Port to use for connection to the target') parser_deploy.set_defaults(func=deploy) - parser_undeploy = subparsers.add_parser('undeploy-target', help='Undeploy recipe output files in live target machine') - parser_undeploy.add_argument('recipename', help='Recipe to undeploy') + parser_undeploy = subparsers.add_parser('undeploy-target', + help='Undeploy recipe output files in live target machine', + description='Un-deploys recipe output files previously deployed to a live target machine by devtool deploy-target.', + group='testbuild') + parser_undeploy.add_argument('recipename', help='Recipe to undeploy (if not using -a/--all)', nargs='?') parser_undeploy.add_argument('target', help='Live target machine running an ssh server: user@hostname') parser_undeploy.add_argument('-c', '--no-host-check', help='Disable ssh host key checking', action='store_true') parser_undeploy.add_argument('-s', '--show-status', help='Show progress/status output', action='store_true') + parser_undeploy.add_argument('-a', '--all', help='Undeploy all recipes deployed on the target', action='store_true') parser_undeploy.add_argument('-n', '--dry-run', help='List files to be undeployed only', action='store_true') + parser_undeploy.add_argument('-P', '--port', default='22', help='Port to use for connection to the target') parser_undeploy.set_defaults(func=undeploy) diff --git a/scripts/lib/devtool/package.py b/scripts/lib/devtool/package.py index a296fce9b1..af9e8f15f5 100644 --- a/scripts/lib/devtool/package.py +++ b/scripts/lib/devtool/package.py @@ -28,15 +28,13 @@ def package(args, config, basepath, workspace): """Entry point for the devtool 'package' subcommand""" check_workspace_recipe(workspace, args.recipename) - tinfoil = setup_tinfoil(basepath=basepath) + tinfoil = setup_tinfoil(basepath=basepath, config_only=True) try: - tinfoil.prepare(config_only=True) - image_pkgtype = config.get('Package', 'image_pkgtype', '') if not image_pkgtype: - image_pkgtype = tinfoil.config_data.getVar('IMAGE_PKGTYPE', True) + image_pkgtype = tinfoil.config_data.getVar('IMAGE_PKGTYPE') - deploy_dir_pkg = tinfoil.config_data.getVar('DEPLOY_DIR_%s' % image_pkgtype.upper(), True) + deploy_dir_pkg = tinfoil.config_data.getVar('DEPLOY_DIR_%s' % image_pkgtype.upper()) finally: tinfoil.shutdown() @@ -54,6 +52,9 @@ def package(args, config, basepath, workspace): def register_commands(subparsers, context): """Register devtool subcommands from the package plugin""" if context.fixed_setup: - parser_package = subparsers.add_parser('package', help='Build packages for a recipe', description='Builds packages for a recipe\'s output files') + parser_package = subparsers.add_parser('package', + help='Build packages for a recipe', + description='Builds packages for a recipe\'s output files', + group='testbuild', order=-5) parser_package.add_argument('recipename', help='Recipe to package') parser_package.set_defaults(func=package) diff --git a/scripts/lib/devtool/runqemu.py b/scripts/lib/devtool/runqemu.py index 5282afba68..e26cf28c2f 100644 --- a/scripts/lib/devtool/runqemu.py +++ b/scripts/lib/devtool/runqemu.py @@ -30,9 +30,13 @@ def runqemu(args, config, basepath, workspace): """Entry point for the devtool 'runqemu' subcommand""" tinfoil = setup_tinfoil(config_only=True, basepath=basepath) - machine = tinfoil.config_data.getVar('MACHINE', True) - bindir_native = tinfoil.config_data.getVar('STAGING_BINDIR_NATIVE', True) - tinfoil.shutdown() + try: + machine = tinfoil.config_data.getVar('MACHINE') + bindir_native = os.path.join(tinfoil.config_data.getVar('STAGING_DIR'), + tinfoil.config_data.getVar('BUILD_ARCH'), + tinfoil.config_data.getVar('bindir_native').lstrip(os.path.sep)) + finally: + tinfoil.shutdown() if not glob.glob(os.path.join(bindir_native, 'qemu-system-*')): raise DevtoolError('QEMU is not available within this SDK') @@ -46,7 +50,12 @@ def runqemu(args, config, basepath, workspace): raise DevtoolError('Unable to determine image name to run, please specify one') try: - exec_build_env_command(config.init_path, basepath, 'runqemu %s %s %s' % (machine, imagename, " ".join(args.args)), watch=True) + # FIXME runqemu assumes that if OECORE_NATIVE_SYSROOT is set then it shouldn't + # run bitbake to find out the values of various environment variables, which + # isn't the case for the extensible SDK. Work around it for now. + newenv = dict(os.environ) + newenv.pop('OECORE_NATIVE_SYSROOT', '') + exec_build_env_command(config.init_path, basepath, 'runqemu %s %s %s' % (machine, imagename, " ".join(args.args)), watch=True, env=newenv) except bb.process.ExecutionError as e: # We've already seen the output since watch=True, so just ensure we return something to the user return e.exitcode @@ -57,7 +66,8 @@ def register_commands(subparsers, context): """Register devtool subcommands from this plugin""" if context.fixed_setup: parser_runqemu = subparsers.add_parser('runqemu', help='Run QEMU on the specified image', - description='Runs QEMU to boot the specified image') + description='Runs QEMU to boot the specified image', + group='testbuild', order=-20) parser_runqemu.add_argument('imagename', help='Name of built image to boot within QEMU', nargs='?') parser_runqemu.add_argument('args', help='Any remaining arguments are passed to the runqemu script (pass --help after imagename to see what these are)', nargs=argparse.REMAINDER) diff --git a/scripts/lib/devtool/sdk.py b/scripts/lib/devtool/sdk.py index 12de9423e7..e8bf0ad98c 100644 --- a/scripts/lib/devtool/sdk.py +++ b/scripts/lib/devtool/sdk.py @@ -1,4 +1,19 @@ # Development tool - sdk-update command plugin +# +# Copyright (C) 2015-2016 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 subprocess @@ -8,6 +23,7 @@ import shutil import errno import sys import tempfile +import re from devtool import exec_build_env_command, setup_tinfoil, parse_recipe, DevtoolError logger = logging.getLogger('devtool') @@ -91,12 +107,10 @@ def check_manifest(fn, basepath): return changedfiles def sdk_update(args, config, basepath, workspace): - # Fetch locked-sigs.inc file from remote/local destination + """Entry point for devtool sdk-update command""" updateserver = args.updateserver if not updateserver: updateserver = config.get('SDK', 'updateserver', '') - if not updateserver: - raise DevtoolError("Update server not specified in config file, you must specify it on the command line") logger.debug("updateserver: %s" % updateserver) # Make sure we are using sdk-update from within SDK @@ -108,10 +122,9 @@ def sdk_update(args, config, basepath, workspace): else: logger.debug("Found conf/locked-sigs.inc in %s" % basepath) - if ':' in updateserver: - is_remote = True - else: - is_remote = False + if not '://' in updateserver: + logger.error("Update server must be a URL") + return -1 layers_dir = os.path.join(basepath, 'layers') conf_dir = os.path.join(basepath, 'conf') @@ -119,94 +132,91 @@ def sdk_update(args, config, basepath, workspace): # Grab variable values tinfoil = setup_tinfoil(config_only=True, basepath=basepath) try: - stamps_dir = tinfoil.config_data.getVar('STAMPS_DIR', True) - sstate_mirrors = tinfoil.config_data.getVar('SSTATE_MIRRORS', True) - site_conf_version = tinfoil.config_data.getVar('SITE_CONF_VERSION', True) + stamps_dir = tinfoil.config_data.getVar('STAMPS_DIR') + sstate_mirrors = tinfoil.config_data.getVar('SSTATE_MIRRORS') + site_conf_version = tinfoil.config_data.getVar('SITE_CONF_VERSION') finally: tinfoil.shutdown() - if not is_remote: - # devtool sdk-update /local/path/to/latest/sdk - new_locked_sig_file_path = os.path.join(updateserver, 'conf/locked-sigs.inc') - if not os.path.exists(new_locked_sig_file_path): - logger.error("%s doesn't exist or is not an extensible SDK" % updateserver) - return -1 - else: - logger.debug("Found conf/locked-sigs.inc in %s" % updateserver) - update_dict = generate_update_dict(new_locked_sig_file_path, old_locked_sig_file_path) - logger.debug("update_dict = %s" % update_dict) - sstate_dir = os.path.join(newsdk_path, 'sstate-cache') - if not os.path.exists(sstate_dir): - logger.error("sstate-cache directory not found under %s" % newsdk_path) - return 1 - sstate_objects = get_sstate_objects(update_dict, sstate_dir) - logger.debug("sstate_objects = %s" % sstate_objects) - if len(sstate_objects) == 0: - logger.info("No need to update.") + tmpsdk_dir = tempfile.mkdtemp() + try: + os.makedirs(os.path.join(tmpsdk_dir, 'conf')) + new_locked_sig_file_path = os.path.join(tmpsdk_dir, 'conf', 'locked-sigs.inc') + # Fetch manifest from server + tmpmanifest = os.path.join(tmpsdk_dir, 'conf', 'sdk-conf-manifest') + ret = subprocess.call("wget -q -O %s %s/conf/sdk-conf-manifest" % (tmpmanifest, updateserver), shell=True) + changedfiles = check_manifest(tmpmanifest, basepath) + if not changedfiles: + logger.info("Already up-to-date") return 0 - logger.info("Installing sstate objects into %s", basepath) - install_sstate_objects(sstate_objects, updateserver.rstrip('/'), basepath) - logger.info("Updating configuration files") - new_conf_dir = os.path.join(updateserver, 'conf') - shutil.rmtree(conf_dir) - shutil.copytree(new_conf_dir, conf_dir) - logger.info("Updating layers") - new_layers_dir = os.path.join(updateserver, 'layers') - shutil.rmtree(layers_dir) - ret = subprocess.call("cp -a %s %s" % (new_layers_dir, layers_dir), shell=True) - if ret != 0: - logger.error("Copying %s to %s failed" % (new_layers_dir, layers_dir)) - return ret - else: - # devtool sdk-update http://myhost/sdk - tmpsdk_dir = tempfile.mkdtemp() - try: - os.makedirs(os.path.join(tmpsdk_dir, 'conf')) - new_locked_sig_file_path = os.path.join(tmpsdk_dir, 'conf', 'locked-sigs.inc') - # Fetch manifest from server - tmpmanifest = os.path.join(tmpsdk_dir, 'conf', 'sdk-conf-manifest') - ret = subprocess.call("wget -q -O %s %s/conf/sdk-conf-manifest" % (tmpmanifest, updateserver), shell=True) - changedfiles = check_manifest(tmpmanifest, basepath) - if not changedfiles: - logger.info("Already up-to-date") - return 0 - # Update metadata - logger.debug("Updating metadata via git ...") - # Try using 'git pull', if failed, use 'git clone' - if os.path.exists(os.path.join(basepath, 'layers/.git')): - ret = subprocess.call("git pull %s/layers/.git" % updateserver, shell=True, cwd=layers_dir) + # Update metadata + logger.debug("Updating metadata via git ...") + #Check for the status before doing a fetch and reset + if os.path.exists(os.path.join(basepath, 'layers/.git')): + out = subprocess.check_output("git status --porcelain", shell=True, cwd=layers_dir) + if not out: + ret = subprocess.call("git fetch --all; git reset --hard", shell=True, cwd=layers_dir) else: - ret = -1 + logger.error("Failed to update metadata as there have been changes made to it. Aborting."); + logger.error("Changed files:\n%s" % out); + return -1 + else: + ret = -1 + if ret != 0: + ret = subprocess.call("git clone %s/layers/.git" % updateserver, shell=True, cwd=tmpsdk_dir) + if ret != 0: + logger.error("Updating metadata via git failed") + return ret + logger.debug("Updating conf files ...") + for changedfile in changedfiles: + ret = subprocess.call("wget -q -O %s %s/%s" % (changedfile, updateserver, changedfile), shell=True, cwd=tmpsdk_dir) if ret != 0: - ret = subprocess.call("git clone %s/layers/.git" % updateserver, shell=True, cwd=tmpsdk_dir) - if ret != 0: - logger.error("Updating metadata via git failed") - return ret - logger.debug("Updating conf files ...") - for changedfile in changedfiles: - ret = subprocess.call("wget -q -O %s %s/%s" % (changedfile, updateserver, changedfile), shell=True, cwd=tmpsdk_dir) - if ret != 0: - logger.error("Updating %s failed" % changedfile) - return ret + logger.error("Updating %s failed" % changedfile) + return ret + + # Check if UNINATIVE_CHECKSUM changed + uninative = False + if 'conf/local.conf' in changedfiles: + def read_uninative_checksums(fn): + chksumitems = [] + with open(fn, 'r') as f: + for line in f: + if line.startswith('UNINATIVE_CHECKSUM'): + splitline = re.split(r'[\[\]"\']', line) + if len(splitline) > 3: + chksumitems.append((splitline[1], splitline[3])) + return chksumitems - # Ok, all is well at this point - move everything over - tmplayers_dir = os.path.join(tmpsdk_dir, 'layers') - if os.path.exists(tmplayers_dir): - shutil.rmtree(layers_dir) - shutil.move(tmplayers_dir, layers_dir) - for changedfile in changedfiles: - destfile = os.path.join(basepath, changedfile) - os.remove(destfile) - shutil.move(os.path.join(tmpsdk_dir, changedfile), destfile) - os.remove(os.path.join(conf_dir, 'sdk-conf-manifest')) - shutil.move(tmpmanifest, conf_dir) + oldsums = read_uninative_checksums(os.path.join(basepath, 'conf/local.conf')) + newsums = read_uninative_checksums(os.path.join(tmpsdk_dir, 'conf/local.conf')) + if oldsums != newsums: + uninative = True + for buildarch, chksum in newsums: + uninative_file = os.path.join('downloads', 'uninative', chksum, '%s-nativesdk-libc.tar.bz2' % buildarch) + mkdir(os.path.join(tmpsdk_dir, os.path.dirname(uninative_file))) + ret = subprocess.call("wget -q -O %s %s/%s" % (uninative_file, updateserver, uninative_file), shell=True, cwd=tmpsdk_dir) - if not sstate_mirrors: - with open(os.path.join(conf_dir, 'site.conf'), 'a') as f: - f.write('SCONF_VERSION = "%s"\n' % site_conf_version) - f.write('SSTATE_MIRRORS_append = " file://.* %s/sstate-cache/PATH \\n "\n' % updateserver) - finally: - shutil.rmtree(tmpsdk_dir) + # Ok, all is well at this point - move everything over + tmplayers_dir = os.path.join(tmpsdk_dir, 'layers') + if os.path.exists(tmplayers_dir): + shutil.rmtree(layers_dir) + shutil.move(tmplayers_dir, layers_dir) + for changedfile in changedfiles: + destfile = os.path.join(basepath, changedfile) + os.remove(destfile) + shutil.move(os.path.join(tmpsdk_dir, changedfile), destfile) + os.remove(os.path.join(conf_dir, 'sdk-conf-manifest')) + shutil.move(tmpmanifest, conf_dir) + if uninative: + shutil.rmtree(os.path.join(basepath, 'downloads', 'uninative')) + shutil.move(os.path.join(tmpsdk_dir, 'downloads', 'uninative'), os.path.join(basepath, 'downloads')) + + if not sstate_mirrors: + with open(os.path.join(conf_dir, 'site.conf'), 'a') as f: + f.write('SCONF_VERSION = "%s"\n' % site_conf_version) + f.write('SSTATE_MIRRORS_append = " file://.* %s/sstate-cache/PATH \\n "\n' % updateserver) + finally: + shutil.rmtree(tmpsdk_dir) if not args.skip_prepare: # Find all potentially updateable tasks @@ -263,7 +273,7 @@ def sdk_install(args, config, basepath, workspace): rd = parse_recipe(config, tinfoil, recipe, True) if not rd: return 1 - stampprefixes[recipe] = '%s.%s' % (rd.getVar('STAMP', True), tasks[0]) + stampprefixes[recipe] = '%s.%s' % (rd.getVar('STAMP'), tasks[0]) if checkstamp(recipe): logger.info('%s is already installed' % recipe) else: @@ -279,8 +289,11 @@ def sdk_install(args, config, basepath, workspace): if recipe.endswith('-native') and 'package' in task: continue install_tasks.append('%s:%s' % (recipe, task)) + options = '' + if not args.allow_build: + options += ' --setscene-only' try: - exec_build_env_command(config.init_path, basepath, 'bitbake --setscene-only %s' % ' '.join(install_tasks)) + exec_build_env_command(config.init_path, basepath, 'bitbake %s %s' % (options, ' '.join(install_tasks)), watch=True) except bb.process.ExecutionError as e: raise DevtoolError('Failed to install %s:\n%s' % (recipe, str(e))) failed = False @@ -293,13 +306,31 @@ def sdk_install(args, config, basepath, workspace): if failed: return 2 + try: + exec_build_env_command(config.init_path, basepath, 'bitbake build-sysroots', watch=True) + except bb.process.ExecutionError as e: + raise DevtoolError('Failed to bitbake build-sysroots:\n%s' % (str(e))) + + def register_commands(subparsers, context): """Register devtool subcommands from the sdk plugin""" if context.fixed_setup: - parser_sdk = subparsers.add_parser('sdk-update', help='Update SDK components from a nominated location') - parser_sdk.add_argument('updateserver', help='The update server to fetch latest SDK components from', nargs='?') + parser_sdk = subparsers.add_parser('sdk-update', + help='Update SDK components', + description='Updates installed SDK components from a remote server', + group='sdk') + updateserver = context.config.get('SDK', 'updateserver', '') + if updateserver: + parser_sdk.add_argument('updateserver', help='The update server to fetch latest SDK components from (default %s)' % updateserver, nargs='?') + else: + parser_sdk.add_argument('updateserver', help='The update server to fetch latest SDK components from') parser_sdk.add_argument('--skip-prepare', action="store_true", help='Skip re-preparing the build system after updating (for debugging only)') parser_sdk.set_defaults(func=sdk_update) - parser_sdk_install = subparsers.add_parser('sdk-install', help='Install additional SDK components', description='Installs additional recipe development files into the SDK. (You can use "devtool search" to find available recipes.)') + + parser_sdk_install = subparsers.add_parser('sdk-install', + help='Install additional SDK components', + description='Installs additional recipe development files into the SDK. (You can use "devtool search" to find available recipes.)', + group='sdk') parser_sdk_install.add_argument('recipename', help='Name of the recipe to install the development artifacts for', nargs='+') + parser_sdk_install.add_argument('-s', '--allow-build', help='Allow building requested item(s) from source', action='store_true') parser_sdk_install.set_defaults(func=sdk_install) diff --git a/scripts/lib/devtool/search.py b/scripts/lib/devtool/search.py index 2ea446237e..054985b85d 100644 --- a/scripts/lib/devtool/search.py +++ b/scripts/lib/devtool/search.py @@ -31,7 +31,7 @@ def search(args, config, basepath, workspace): tinfoil = setup_tinfoil(config_only=False, basepath=basepath) try: - pkgdata_dir = tinfoil.config_data.getVar('PKGDATA_DIR', True) + pkgdata_dir = tinfoil.config_data.getVar('PKGDATA_DIR') defsummary = tinfoil.config_data.getVar('SUMMARY', False) or '' keyword_rc = re.compile(args.keyword) @@ -70,7 +70,7 @@ def search(args, config, basepath, workspace): if match: rd = parse_recipe(config, tinfoil, fn, True) - summary = rd.getVar('SUMMARY', True) + summary = rd.getVar('SUMMARY') if summary == rd.expand(defsummary): summary = '' print("%s %s" % (fn.ljust(20), summary)) @@ -82,6 +82,7 @@ def search(args, config, basepath, workspace): def register_commands(subparsers, context): """Register devtool subcommands from this plugin""" parser_search = subparsers.add_parser('search', help='Search available recipes', - description='Searches for available target recipes. Matches on recipe name, package name, description and installed files, and prints the recipe name on match.') + description='Searches for available target recipes. Matches on recipe name, package name, description and installed files, and prints the recipe name on match.', + group='info') parser_search.add_argument('keyword', help='Keyword to search for (regular expression syntax allowed)') parser_search.set_defaults(func=search, no_workspace=True) diff --git a/scripts/lib/devtool/standard.py b/scripts/lib/devtool/standard.py index d12cc2e222..5ff1e230fd 100644 --- a/scripts/lib/devtool/standard.py +++ b/scripts/lib/devtool/standard.py @@ -1,6 +1,6 @@ # Development tool - standard commands plugin # -# Copyright (C) 2014-2015 Intel Corporation +# Copyright (C) 2014-2016 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 @@ -20,6 +20,7 @@ import os import sys import re import shutil +import subprocess import tempfile import logging import argparse @@ -27,8 +28,9 @@ import argparse_oe import scriptutils import errno import glob +import filecmp from collections import OrderedDict -from devtool import exec_build_env_command, setup_tinfoil, check_workspace_recipe, use_external_build, setup_git_repo, recipe_to_append, get_bbclassextend_targets, DevtoolError +from devtool import exec_build_env_command, setup_tinfoil, check_workspace_recipe, use_external_build, setup_git_repo, recipe_to_append, get_bbclassextend_targets, ensure_npm, DevtoolError from devtool import parse_recipe logger = logging.getLogger('devtool') @@ -45,13 +47,13 @@ def add(args, config, basepath, workspace): # These are positional arguments, but because we're nice, allow # specifying e.g. source tree without name, or fetch URI without name or # source tree (if we can detect that that is what the user meant) - if '://' in args.recipename: + if scriptutils.is_src_url(args.recipename): if not args.fetchuri: if args.fetch: raise DevtoolError('URI specified as positional argument as well as -f/--fetch') args.fetchuri = args.recipename args.recipename = '' - elif args.srctree and '://' in args.srctree: + elif scriptutils.is_src_url(args.srctree): if not args.fetchuri: if args.fetch: raise DevtoolError('URI specified as positional argument as well as -f/--fetch') @@ -62,13 +64,17 @@ def add(args, config, basepath, workspace): args.srctree = args.recipename args.recipename = None elif os.path.isdir(args.recipename): - logger.warn('Ambiguous argument %s - assuming you mean it to be the recipe name') + logger.warn('Ambiguous argument "%s" - assuming you mean it to be the recipe name' % args.recipename) + + if args.srctree and os.path.isfile(args.srctree): + args.fetchuri = 'file://' + os.path.abspath(args.srctree) + args.srctree = '' if args.fetch: if args.fetchuri: raise DevtoolError('URI specified as positional argument as well as -f/--fetch') else: - # FIXME should show a warning that -f/--fetch is deprecated here + logger.warn('-f/--fetch option is deprecated - you can now simply specify the URL to fetch as a positional argument instead') args.fetchuri = args.fetch if args.recipename: @@ -79,10 +85,6 @@ def add(args, config, basepath, workspace): if reason: raise DevtoolError(reason) - # FIXME this ought to be in validate_pn but we're using that in other contexts - if '/' in args.recipename: - raise DevtoolError('"/" is not a valid character in recipe names') - if args.srctree: srctree = os.path.abspath(args.srctree) srctreeparent = None @@ -126,6 +128,9 @@ def add(args, config, basepath, workspace): color = args.color extracmdopts = '' if args.fetchuri: + if args.fetchuri.startswith('npm://'): + ensure_npm(config, basepath, args.fixed_setup) + source = args.fetchuri if srctree: extracmdopts += ' -x %s' % srctree @@ -143,16 +148,35 @@ def add(args, config, basepath, workspace): extracmdopts += ' --also-native' if args.src_subdir: extracmdopts += ' --src-subdir "%s"' % args.src_subdir + if args.autorev: + extracmdopts += ' -a' + if args.fetch_dev: + extracmdopts += ' --fetch-dev' tempdir = tempfile.mkdtemp(prefix='devtool') try: - try: - stdout, _ = exec_build_env_command(config.init_path, basepath, 'recipetool --color=%s create -o %s "%s" %s' % (color, tempdir, source, extracmdopts)) - except bb.process.ExecutionError as e: - if e.exitcode == 15: - raise DevtoolError('Unable to auto-determine name from source tree, please specify it with -N/--name') - else: - raise DevtoolError('Command \'%s\' failed:\n%s' % (e.command, e.stdout)) + builtnpm = False + while True: + try: + stdout, _ = exec_build_env_command(config.init_path, basepath, 'recipetool --color=%s create --devtool -o %s \'%s\' %s' % (color, tempdir, source, extracmdopts), watch=True) + except bb.process.ExecutionError as e: + if e.exitcode == 14: + if builtnpm: + raise DevtoolError('Re-running recipetool still failed to find npm') + # FIXME this is a horrible hack that is unfortunately + # necessary due to the fact that we can't run bitbake from + # inside recipetool since recipetool keeps tinfoil active + # with references to it throughout the code, so we have + # to exit out and come back here to do it. + ensure_npm(config, basepath, args.fixed_setup, check_exists=False) + logger.info('Re-running recipe creation process after building nodejs') + builtnpm = True + continue + elif e.exitcode == 15: + raise DevtoolError('Could not auto-determine recipe name, please specify it on the command line') + else: + raise DevtoolError('Command \'%s\' failed' % e.command) + break recipes = glob.glob(os.path.join(tempdir, '*.bb')) if recipes: @@ -188,9 +212,12 @@ def add(args, config, basepath, workspace): raise DevtoolError('Couldn\'t find source tree created by recipetool') bb.utils.mkdirhier(recipedir) shutil.move(recipes[0], recipefile) + # Move any additional files created by recipetool + for fn in os.listdir(tempdir): + shutil.move(os.path.join(tempdir, fn), recipedir) else: raise DevtoolError('Command \'%s\' did not create any recipe file:\n%s' % (e.command, e.stdout)) - attic_recipe = os.path.join(config.workspace_path, 'attic', os.path.basename(recipefile)) + attic_recipe = os.path.join(config.workspace_path, 'attic', recipename, os.path.basename(recipefile)) if os.path.exists(attic_recipe): logger.warn('A modified recipe from a previous invocation exists in %s - you may wish to move this over the top of the new recipe if you had changes in it that you want to continue with' % attic_recipe) finally: @@ -198,46 +225,69 @@ def add(args, config, basepath, workspace): shutil.rmtree(tmpsrcdir) shutil.rmtree(tempdir) - _add_md5(config, recipename, recipefile) - - if args.fetch and not args.no_git: - setup_git_repo(srctree, args.version, 'devtool') - - 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() + for fn in os.listdir(recipedir): + _add_md5(config, recipename, os.path.join(recipedir, fn)) tinfoil = setup_tinfoil(config_only=True, basepath=basepath) - rd = oe.recipeutils.parse_recipe(recipefile, None, tinfoil.config_data) - if not rd: - return 1 - - if args.src_subdir: - srctree = os.path.join(srctree, args.src_subdir) - - bb.utils.mkdirhier(os.path.dirname(appendfile)) - with open(appendfile, 'w') as f: - f.write('inherit externalsrc\n') - f.write('EXTERNALSRC = "%s"\n' % srctree) - - b_is_s = use_external_build(args.same_dir, args.no_same_dir, rd) - if b_is_s: - f.write('EXTERNALSRC_BUILD = "%s"\n' % srctree) - if initial_rev: - f.write('\n# initial_rev: %s\n' % initial_rev) - - if args.binary: - f.write('do_install_append() {\n') - f.write(' rm -rf ${D}/.git\n') - f.write(' rm -f ${D}/singletask.lock\n') - f.write('}\n') + try: + try: + rd = tinfoil.parse_recipe_file(recipefile, False) + except Exception as e: + logger.error(str(e)) + rd = None + if not rd: + # Parsing failed. We just created this recipe and we shouldn't + # leave it in the workdir or it'll prevent bitbake from starting + movefn = '%s.parsefailed' % recipefile + logger.error('Parsing newly created recipe failed, moving recipe to %s for reference. If this looks to be caused by the recipe itself, please report this error.' % movefn) + shutil.move(recipefile, movefn) + return 1 - _add_md5(config, recipename, appendfile) + if args.fetchuri and not args.no_git: + setup_git_repo(srctree, args.version, 'devtool', d=tinfoil.config_data) - logger.info('Recipe %s has been automatically created; further editing may be required to make it fully functional' % 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() + + if args.src_subdir: + srctree = os.path.join(srctree, args.src_subdir) + + bb.utils.mkdirhier(os.path.dirname(appendfile)) + with open(appendfile, 'w') as f: + f.write('inherit externalsrc\n') + f.write('EXTERNALSRC = "%s"\n' % srctree) + + b_is_s = use_external_build(args.same_dir, args.no_same_dir, rd) + if b_is_s: + f.write('EXTERNALSRC_BUILD = "%s"\n' % srctree) + if initial_rev: + f.write('\n# initial_rev: %s\n' % initial_rev) + + if args.binary: + f.write('do_install_append() {\n') + f.write(' rm -rf ${D}/.git\n') + f.write(' rm -f ${D}/singletask.lock\n') + f.write('}\n') + + if bb.data.inherits_class('npm', rd): + f.write('do_install_append() {\n') + f.write(' # Remove files added to source dir by devtool/externalsrc\n') + f.write(' rm -f ${NPM_INSTALLDIR}/singletask.lock\n') + f.write(' rm -rf ${NPM_INSTALLDIR}/.git\n') + f.write(' rm -rf ${NPM_INSTALLDIR}/oe-local-files\n') + f.write(' for symlink in ${EXTERNALSRC_SYMLINKS} ; do\n') + f.write(' rm -f ${NPM_INSTALLDIR}/${symlink%%:*}\n') + f.write(' done\n') + f.write('}\n') + + _add_md5(config, recipename, appendfile) + + logger.info('Recipe %s has been automatically created; further editing may be required to make it fully functional' % recipefile) - tinfoil.shutdown() + finally: + tinfoil.shutdown() return 0 @@ -246,28 +296,30 @@ def _check_compatible_recipe(pn, d): """Check if the recipe is supported by devtool""" if pn == 'perf': raise DevtoolError("The perf recipe does not actually check out " - "source and thus cannot be supported by this tool") + "source and thus cannot be supported by this tool", + 4) if pn in ['kernel-devsrc', 'package-index'] or pn.startswith('gcc-source'): - raise DevtoolError("The %s recipe is not supported by this tool" % pn) + raise DevtoolError("The %s recipe is not supported by this tool" % pn, 4) if bb.data.inherits_class('image', d): raise DevtoolError("The %s recipe is an image, and therefore is not " - "supported by this tool" % pn) + "supported by this tool" % pn, 4) if bb.data.inherits_class('populate_sdk', d): raise DevtoolError("The %s recipe is an SDK, and therefore is not " - "supported by this tool" % pn) + "supported by this tool" % pn, 4) if bb.data.inherits_class('packagegroup', d): raise DevtoolError("The %s recipe is a packagegroup, and therefore is " - "not supported by this tool" % pn) + "not supported by this tool" % pn, 4) if bb.data.inherits_class('meta', d): raise DevtoolError("The %s recipe is a meta-recipe, and therefore is " - "not supported by this tool" % pn) + "not supported by this tool" % pn, 4) - if bb.data.inherits_class('externalsrc', d) and d.getVar('EXTERNALSRC', True): + if bb.data.inherits_class('externalsrc', d) and d.getVar('EXTERNALSRC'): + # Not an incompatibility error per se, so we don't pass the error code raise DevtoolError("externalsrc is currently enabled for the %s " "recipe. This prevents the normal do_patch task " "from working. You will need to disable this " @@ -280,6 +332,13 @@ def _move_file(src, dst): bb.utils.mkdirhier(dst_d) shutil.move(src, dst) +def _copy_file(src, dst): + """Copy a file. Creates all the directory components of destination path.""" + dst_d = os.path.dirname(dst) + if dst_d: + bb.utils.mkdirhier(dst_d) + shutil.copy(src, dst) + def _git_ls_tree(repodir, treeish='HEAD', recursive=False): """List contents of a git treeish""" import bb @@ -288,10 +347,11 @@ def _git_ls_tree(repodir, treeish='HEAD', recursive=False): cmd.append('-r') out, _ = bb.process.run(cmd, cwd=repodir) ret = {} - for line in out.split('\0'): - if line: - split = line.split(None, 4) - ret[split[3]] = split[0:3] + if out: + for line in out.split('\0'): + if line: + split = line.split(None, 4) + ret[split[3]] = split[0:3] return ret def _git_exclude_path(srctree, path): @@ -303,7 +363,7 @@ def _git_exclude_path(srctree, path): # becomes greater than that. path = os.path.normpath(path) recurse = True if len(path.split(os.path.sep)) > 1 else False - git_files = _git_ls_tree(srctree, 'HEAD', recurse).keys() + git_files = list(_git_ls_tree(srctree, 'HEAD', recurse).keys()) if path in git_files: git_files.remove(path) return git_files @@ -327,19 +387,21 @@ def extract(args, config, basepath, workspace): if not tinfoil: # Error already shown return 1 + try: + rd = parse_recipe(config, tinfoil, args.recipename, True) + if not rd: + return 1 - rd = parse_recipe(config, tinfoil, args.recipename, True) - if not rd: - return 1 - - srctree = os.path.abspath(args.srctree) - initial_rev = _extract_source(srctree, args.keep_temp, args.branch, False, rd) - logger.info('Source tree extracted to %s' % srctree) + srctree = os.path.abspath(args.srctree) + initial_rev = _extract_source(srctree, args.keep_temp, args.branch, False, rd, tinfoil) + logger.info('Source tree extracted to %s' % srctree) - if initial_rev: - return 0 - else: - return 1 + if initial_rev: + return 0 + else: + return 1 + finally: + tinfoil.shutdown() def sync(args, config, basepath, workspace): """Entry point for the devtool 'sync' subcommand""" @@ -349,86 +411,29 @@ def sync(args, config, basepath, workspace): if not tinfoil: # Error already shown return 1 + try: + rd = parse_recipe(config, tinfoil, args.recipename, True) + if not rd: + return 1 - rd = parse_recipe(config, tinfoil, args.recipename, True) - if not rd: - return 1 - - srctree = os.path.abspath(args.srctree) - initial_rev = _extract_source(srctree, args.keep_temp, args.branch, True, rd) - logger.info('Source tree %s synchronized' % srctree) - - if initial_rev: - return 0 - else: - return 1 - -class BbTaskExecutor(object): - """Class for executing bitbake tasks for a recipe - - 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 - """ - - def __init__(self, rdata): - self.rdata = rdata - self.executed = [] - - def exec_func(self, func, report): - """Run bitbake task function""" - if not func in self.executed: - deps = self.rdata.getVarFlag(func, 'deps', False) - if deps: - for taskdepfunc in deps: - self.exec_func(taskdepfunc, True) - if report: - logger.info('Executing %s...' % func) - fn = self.rdata.getVar('FILE', True) - localdata = bb.build._task_data(fn, func, self.rdata) - try: - bb.build.exec_func(func, localdata) - except bb.build.FuncFailed as e: - raise DevtoolError(str(e)) - self.executed.append(func) - - -class PatchTaskExecutor(BbTaskExecutor): - def __init__(self, rdata): - self.check_git = False - super(PatchTaskExecutor, self).__init__(rdata) - - def exec_func(self, func, report): - from oe.patch import GitApplyTree - srcsubdir = self.rdata.getVar('S', True) - haspatches = False - if func == 'do_patch': - patchdir = os.path.join(srcsubdir, 'patches') - if os.path.exists(patchdir): - if os.listdir(patchdir): - haspatches = True - else: - os.rmdir(patchdir) - - super(PatchTaskExecutor, self).exec_func(func, report) - if self.check_git and os.path.exists(srcsubdir): - if func == 'do_patch': - if os.path.exists(patchdir): - shutil.rmtree(patchdir) - if haspatches: - stdout, _ = bb.process.run('git status --porcelain patches', cwd=srcsubdir) - if stdout: - bb.process.run('git checkout patches', cwd=srcsubdir) + srctree = os.path.abspath(args.srctree) + initial_rev = _extract_source(srctree, args.keep_temp, args.branch, True, rd, tinfoil) + logger.info('Source tree %s synchronized' % srctree) - stdout, _ = bb.process.run('git status --porcelain', cwd=srcsubdir) - if stdout: - bb.process.run('git add .; git commit -a -m "Committing changes from %s\n\n%s"' % (func, GitApplyTree.ignore_commit_prefix + ' - from %s' % func), cwd=srcsubdir) + if initial_rev: + return 0 + else: + return 1 + finally: + tinfoil.shutdown() -def _prep_extract_operation(config, basepath, recipename): +def _prep_extract_operation(config, basepath, recipename, tinfoil=None): """HACK: Ugly workaround for making sure that requirements are met when trying to extract a package. Returns the tinfoil instance to be used.""" - tinfoil = setup_tinfoil(basepath=basepath) + if not tinfoil: + tinfoil = setup_tinfoil(basepath=basepath) + rd = parse_recipe(config, tinfoil, recipename, True) if not rd: return None @@ -445,22 +450,11 @@ def _prep_extract_operation(config, basepath, recipename): return tinfoil -def _extract_source(srctree, keep_temp, devbranch, sync, d): +def _extract_source(srctree, keep_temp, devbranch, sync, d, tinfoil): """Extract sources of a recipe""" - import bb.event import oe.recipeutils - def eventfilter(name, handler, event, d): - """Bitbake event filter for devtool extract operation""" - 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) + pn = d.getVar('PN') _check_compatible_recipe(pn, d) @@ -478,59 +472,112 @@ def _extract_source(srctree, keep_temp, devbranch, sync, d): if 'noexec' in (d.getVarFlags('do_unpack', False) or []): raise DevtoolError("The %s recipe has do_unpack disabled, unable to " - "extract source" % pn) + "extract source" % pn, 4) if not sync: # Prepare for shutil.move later on bb.utils.mkdirhier(srctree) os.rmdir(srctree) - # We don't want notes to be printed, they are too verbose - origlevel = bb.logger.getEffectiveLevel() - if logger.getEffectiveLevel() > logging.DEBUG: - bb.logger.setLevel(logging.WARNING) - initial_rev = None - tempdir = tempfile.mkdtemp(prefix='devtool') + # We need to redirect WORKDIR, STAMPS_DIR etc. under a temporary + # directory so that: + # (a) we pick up all files that get unpacked to the WORKDIR, and + # (b) we don't disturb the existing build + # However, with recipe-specific sysroots the sysroots for the recipe + # will be prepared under WORKDIR, and if we used the system temporary + # directory (i.e. usually /tmp) as used by mkdtemp by default, then + # our attempts to hardlink files into the recipe-specific sysroots + # will fail on systems where /tmp is a different filesystem, and it + # would have to fall back to copying the files which is a waste of + # time. Put the temp directory under the WORKDIR to prevent that from + # being a problem. + tempbasedir = d.getVar('WORKDIR') + bb.utils.mkdirhier(tempbasedir) + tempdir = tempfile.mkdtemp(prefix='devtooltmp-', dir=tempbasedir) try: + tinfoil.logger.setLevel(logging.WARNING) + 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')) - if not crd.getVar('S', True).startswith(workdir): + if not crd.getVar('S').startswith(workdir): # Usually a shared workdir recipe (kernel, gcc) # Try to set a reasonable default if bb.data.inherits_class('kernel', d): crd.setVar('S', '${WORKDIR}/source') else: - crd.setVar('S', '${WORKDIR}/%s' % os.path.basename(d.getVar('S', True))) + crd.setVar('S', '${WORKDIR}/%s' % os.path.basename(d.getVar('S'))) if bb.data.inherits_class('kernel', d): # We don't want to move the source to STAGING_KERNEL_DIR here crd.setVar('STAGING_KERNEL_DIR', '${S}') - task_executor = PatchTaskExecutor(crd) + is_kernel_yocto = bb.data.inherits_class('kernel-yocto', d) + if not is_kernel_yocto: + crd.setVar('PATCHTOOL', 'git') + crd.setVar('PATCH_COMMIT_FUNCTIONS', '1') + + # Apply our changes to the datastore to the server's datastore + for key in crd.localkeys(): + tinfoil.config_data.setVar('%s_pn-%s' % (key, pn), crd.getVar(key, False)) + + tinfoil.config_data.setVar('STAMPS_DIR', os.path.join(tempdir, 'stamps')) + tinfoil.config_data.setVar('T', os.path.join(tempdir, 'temp')) + tinfoil.config_data.setVar('BUILDCFG_FUNCS', '') + tinfoil.config_data.setVar('BUILDCFG_HEADER', '') + tinfoil.config_data.setVar('BB_HASH_IGNORE_MISMATCH', '1') + + tinfoil.set_event_mask(['bb.event.BuildStarted', + 'bb.event.BuildCompleted', + 'logging.LogRecord', + 'bb.command.CommandCompleted', + 'bb.command.CommandFailed', + 'bb.build.TaskStarted', + 'bb.build.TaskSucceeded', + 'bb.build.TaskFailed', + 'bb.build.TaskFailedSilent']) + + def runtask(target, task): + if tinfoil.build_file(target, task): + while True: + event = tinfoil.wait_event(0.25) + if event: + if isinstance(event, bb.command.CommandCompleted): + break + elif isinstance(event, bb.command.CommandFailed): + raise DevtoolError('Task do_%s failed: %s' % (task, event.error)) + elif isinstance(event, bb.build.TaskFailed): + raise DevtoolError('Task do_%s failed' % task) + elif isinstance(event, bb.build.TaskStarted): + logger.info('Executing %s...' % event._task) + elif isinstance(event, logging.LogRecord): + if event.levelno <= logging.INFO: + continue + logger.handle(event) - crd.setVar('EXTERNALSRC_forcevariable', '') + # we need virtual:native:/path/to/recipe if it's a BBCLASSEXTEND + fn = tinfoil.get_recipe_file(pn) + runtask(fn, 'unpack') - logger.info('Fetching %s...' % pn) - task_executor.exec_func('do_fetch', False) - logger.info('Unpacking...') - task_executor.exec_func('do_unpack', False) if bb.data.inherits_class('kernel-yocto', d): # Extra step for kernel to populate the source directory - logger.info('Doing kernel checkout...') - task_executor.exec_func('do_kernel_checkout', False) - srcsubdir = crd.getVar('S', True) + runtask(fn, 'kernel_checkout') - task_executor.check_git = True + srcsubdir = crd.getVar('S') # Move local source files into separate subdir recipe_patches = [os.path.basename(patch) for patch in oe.recipeutils.get_recipe_patches(crd)] local_files = oe.recipeutils.get_recipe_local_files(crd) + + # Ignore local files with subdir={BP} + srcabspath = os.path.abspath(srcsubdir) local_files = [fname for fname in local_files if - os.path.exists(os.path.join(workdir, fname))] + os.path.exists(os.path.join(workdir, fname)) and + (srcabspath == workdir or not + os.path.join(workdir, fname).startswith(srcabspath + + os.sep))] if local_files: for fname in local_files: _move_file(os.path.join(workdir, fname), @@ -547,7 +594,7 @@ def _extract_source(srctree, keep_temp, devbranch, sync, d): os.path.basename(fname) not in recipe_patches] # Force separate S so that patch files can be left out from srctree srcsubdir = tempfile.mkdtemp(dir=workdir) - crd.setVar('S', srcsubdir) + tinfoil.config_data.setVar('S_task-patch', srcsubdir) # Move source files to S for path in src_files: _move_file(os.path.join(workdir, path), @@ -565,24 +612,22 @@ def _extract_source(srctree, keep_temp, devbranch, sync, d): "doesn't use any source or the correct source " "directory could not be determined" % pn) - setup_git_repo(srcsubdir, crd.getVar('PV', True), devbranch) + setup_git_repo(srcsubdir, crd.getVar('PV'), devbranch, d=d) (stdout, _) = bb.process.run('git rev-parse HEAD', cwd=srcsubdir) initial_rev = stdout.rstrip() - crd.setVar('PATCHTOOL', 'git') - logger.info('Patching...') - task_executor.exec_func('do_patch', False) + runtask(fn, 'patch') bb.process.run('git tag -f devtool-patched', cwd=srcsubdir) + kconfig = None if bb.data.inherits_class('kernel-yocto', d): # Store generate and store kernel config logger.info('Generating kernel config') - task_executor.exec_func('do_configure', False) - kconfig = os.path.join(crd.getVar('B', True), '.config') - shutil.copy2(kconfig, srcsubdir) + runtask(fn, 'configure') + kconfig = os.path.join(crd.getVar('B'), '.config') tempdir_localdir = os.path.join(tempdir, 'oe-local-files') @@ -614,9 +659,34 @@ def _extract_source(srctree, keep_temp, devbranch, sync, d): shutil.move(srcsubdir, srctree) - finally: - bb.logger.setLevel(origlevel) + if os.path.abspath(d.getVar('S')) == os.path.abspath(d.getVar('WORKDIR')): + # If recipe extracts to ${WORKDIR}, symlink the files into the srctree + # (otherwise the recipe won't build as expected) + local_files_dir = os.path.join(srctree, 'oe-local-files') + addfiles = [] + for root, _, files in os.walk(local_files_dir): + relpth = os.path.relpath(root, local_files_dir) + if relpth != '.': + bb.utils.mkdirhier(os.path.join(srctree, relpth)) + for fn in files: + if fn == '.gitignore': + continue + destpth = os.path.join(srctree, relpth, fn) + if os.path.exists(destpth): + os.unlink(destpth) + os.symlink('oe-local-files/%s' % fn, destpth) + addfiles.append(os.path.join(relpth, fn)) + if addfiles: + bb.process.run('git add %s' % ' '.join(addfiles), cwd=srctree) + useroptions = [] + oe.patch.GitApplyTree.gitCommandUserOptions(useroptions, d=d) + bb.process.run('git %s commit -a -m "Committing local file symlinks\n\n%s"' % (' '.join(useroptions), oe.patch.GitApplyTree.ignore_commit_prefix), cwd=srctree) + + if kconfig: + logger.info('Copying kernel config to srctree') + shutil.copy2(kconfig, srctree) + finally: if keep_temp: logger.info('Preserving temporary directory %s' % tempdir) else: @@ -645,7 +715,7 @@ 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') + preservepath = os.path.join(config.workspace_path, 'attic', recipename) with open(origfile, 'r') as f: with open(newfile, 'w') as tf: for line in f.readlines(): @@ -680,124 +750,323 @@ def modify(args, config, basepath, workspace): raise DevtoolError("recipe %s is already in your workspace" % args.recipename) - if args.srctree: - srctree = os.path.abspath(args.srctree) - else: - srctree = get_default_srctree(config, args.recipename) - - if args.no_extract and not os.path.isdir(srctree): - raise DevtoolError("--no-extract specified and source path %s does " - "not exist or is not a directory" % - srctree) - if not args.no_extract: - tinfoil = _prep_extract_operation(config, basepath, args.recipename) - if not tinfoil: - # Error already shown + tinfoil = setup_tinfoil(basepath=basepath) + try: + rd = parse_recipe(config, tinfoil, args.recipename, True) + if not rd: return 1 - else: - tinfoil = setup_tinfoil(basepath=basepath) - rd = parse_recipe(config, tinfoil, args.recipename, True) - if not rd: - return 1 + pn = rd.getVar('PN') + if pn != args.recipename: + logger.info('Mapping %s to %s' % (args.recipename, pn)) + if pn in workspace: + raise DevtoolError("recipe %s is already in your workspace" % + pn) - pn = rd.getVar('PN', True) - if pn != args.recipename: - logger.info('Mapping %s to %s' % (args.recipename, pn)) - if pn in workspace: - raise DevtoolError("recipe %s is already in your workspace" % - pn) + if args.srctree: + srctree = os.path.abspath(args.srctree) + else: + srctree = get_default_srctree(config, pn) + + if args.no_extract and not os.path.isdir(srctree): + raise DevtoolError("--no-extract specified and source path %s does " + "not exist or is not a directory" % + srctree) + if not args.no_extract: + tinfoil = _prep_extract_operation(config, basepath, pn, tinfoil) + if not tinfoil: + # Error already shown + return 1 + # We need to re-parse because tinfoil may have been re-initialised + rd = parse_recipe(config, tinfoil, args.recipename, True) + + recipefile = rd.getVar('FILE') + appendfile = recipe_to_append(recipefile, config, args.wildcard) + if os.path.exists(appendfile): + raise DevtoolError("Another variant of recipe %s is already in your " + "workspace (only one variant of a recipe can " + "currently be worked on at once)" + % pn) + + _check_compatible_recipe(pn, rd) + + initial_rev = None + commits = [] + if not args.no_extract: + initial_rev = _extract_source(srctree, args.keep_temp, args.branch, False, rd, tinfoil) + if not initial_rev: + return 1 + logger.info('Source tree extracted to %s' % srctree) + # Get list of commits since this revision + (stdout, _) = bb.process.run('git rev-list --reverse %s..HEAD' % initial_rev, cwd=srctree) + commits = stdout.split() + else: + if os.path.exists(os.path.join(srctree, '.git')): + # Check if it's a tree previously extracted by us + try: + (stdout, _) = bb.process.run('git branch --contains devtool-base', cwd=srctree) + except bb.process.ExecutionError: + stdout = '' + for line in stdout.splitlines(): + if line.startswith('*'): + (stdout, _) = bb.process.run('git rev-parse devtool-base', cwd=srctree) + initial_rev = stdout.rstrip() + if not initial_rev: + # Otherwise, just grab the head revision + (stdout, _) = bb.process.run('git rev-parse HEAD', cwd=srctree) + initial_rev = stdout.rstrip() - recipefile = rd.getVar('FILE', True) - appendfile = recipe_to_append(recipefile, config, args.wildcard) - if os.path.exists(appendfile): - raise DevtoolError("Another variant of recipe %s is already in your " - "workspace (only one variant of a recipe can " - "currently be worked on at once)" - % pn) + # Check that recipe isn't using a shared workdir + s = os.path.abspath(rd.getVar('S')) + workdir = os.path.abspath(rd.getVar('WORKDIR')) + if s.startswith(workdir) and s != workdir and os.path.dirname(s) != workdir: + # Handle if S is set to a subdirectory of the source + srcsubdir = os.path.relpath(s, workdir).split(os.sep, 1)[1] + srctree = os.path.join(srctree, srcsubdir) + + bb.utils.mkdirhier(os.path.dirname(appendfile)) + with open(appendfile, 'w') as f: + f.write('FILESEXTRAPATHS_prepend := "${THISDIR}/${PN}:"\n') + # Local files can be modified/tracked in separate subdir under srctree + # Mostly useful for packages with S != WORKDIR + f.write('FILESPATH_prepend := "%s:"\n' % + os.path.join(srctree, 'oe-local-files')) + + f.write('\ninherit 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' % (pn, srctree)) + + b_is_s = use_external_build(args.same_dir, args.no_same_dir, rd) + if b_is_s: + f.write('EXTERNALSRC_BUILD_pn-%s = "%s"\n' % (pn, srctree)) + + if bb.data.inherits_class('kernel', rd): + f.write('SRCTREECOVEREDTASKS = "do_validate_branches do_kernel_checkout ' + 'do_fetch do_unpack do_patch do_kernel_configme do_kernel_configcheck"\n') + f.write('\ndo_configure_append() {\n' + ' cp ${B}/.config ${S}/.config.baseline\n' + ' ln -sfT ${B}/.config ${S}/.config.new\n' + '}\n') + 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, pn, appendfile) + + logger.info('Recipe %s now set up to build from %s' % (pn, srctree)) - _check_compatible_recipe(pn, rd) + finally: + tinfoil.shutdown() - initial_rev = None - commits = [] - if not args.no_extract: - initial_rev = _extract_source(srctree, False, args.branch, False, rd) - if not initial_rev: - return 1 - logger.info('Source tree extracted to %s' % srctree) - # Get list of commits since this revision - (stdout, _) = bb.process.run('git rev-list --reverse %s..HEAD' % initial_rev, cwd=srctree) - commits = stdout.split() + return 0 + + +def rename(args, config, basepath, workspace): + """Entry point for the devtool 'rename' subcommand""" + import bb + import oe.recipeutils + + check_workspace_recipe(workspace, args.recipename) + + if not (args.newname or args.version): + raise DevtoolError('You must specify a new name, a version with -V/--version, or both') + + recipefile = workspace[args.recipename]['recipefile'] + if not recipefile: + raise DevtoolError('devtool rename can only be used where the recipe file itself is in the workspace (e.g. after devtool add)') + + if args.newname and args.newname != args.recipename: + reason = oe.recipeutils.validate_pn(args.newname) + if reason: + raise DevtoolError(reason) + newname = args.newname else: - if os.path.exists(os.path.join(srctree, '.git')): - # Check if it's a tree previously extracted by us - try: - (stdout, _) = bb.process.run('git branch --contains devtool-base', cwd=srctree) - except bb.process.ExecutionError: - stdout = '' - for line in stdout.splitlines(): - if line.startswith('*'): - (stdout, _) = bb.process.run('git rev-parse devtool-base', cwd=srctree) - initial_rev = stdout.rstrip() - if not initial_rev: - # Otherwise, just grab the head revision - (stdout, _) = bb.process.run('git rev-parse HEAD', cwd=srctree) - initial_rev = stdout.rstrip() - - # Check that recipe isn't using a shared workdir - s = os.path.abspath(rd.getVar('S', True)) - workdir = os.path.abspath(rd.getVar('WORKDIR', True)) - if s.startswith(workdir) and s != workdir and os.path.dirname(s) != workdir: - # Handle if S is set to a subdirectory of the source - srcsubdir = os.path.relpath(s, workdir).split(os.sep, 1)[1] - srctree = os.path.join(srctree, srcsubdir) - - bb.utils.mkdirhier(os.path.dirname(appendfile)) - with open(appendfile, 'w') as f: - f.write('FILESEXTRAPATHS_prepend := "${THISDIR}/${PN}:"\n') - # Local files can be modified/tracked in separate subdir under srctree - # Mostly useful for packages with S != WORKDIR - f.write('FILESPATH_prepend := "%s:"\n' % - os.path.join(srctree, 'oe-local-files')) - - f.write('\ninherit 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' % (pn, srctree)) - - b_is_s = use_external_build(args.same_dir, args.no_same_dir, rd) - if b_is_s: - f.write('EXTERNALSRC_BUILD_pn-%s = "%s"\n' % (pn, srctree)) - - if bb.data.inherits_class('kernel', rd): - f.write('SRCTREECOVEREDTASKS = "do_validate_branches do_kernel_checkout ' - 'do_fetch do_unpack do_patch do_kernel_configme do_kernel_configcheck"\n') - if initial_rev: - f.write('\n# initial_rev: %s\n' % initial_rev) - for commit in commits: - f.write('# commit: %s\n' % commit) + newname = args.recipename + + append = workspace[args.recipename]['bbappend'] + appendfn = os.path.splitext(os.path.basename(append))[0] + splitfn = appendfn.split('_') + if len(splitfn) > 1: + origfnver = appendfn.split('_')[1] + else: + origfnver = '' + + recipefilemd5 = None + tinfoil = setup_tinfoil(basepath=basepath, tracking=True) + try: + rd = parse_recipe(config, tinfoil, args.recipename, True) + if not rd: + return 1 + + bp = rd.getVar('BP') + bpn = rd.getVar('BPN') + if newname != args.recipename: + localdata = rd.createCopy() + localdata.setVar('PN', newname) + newbpn = localdata.getVar('BPN') + else: + newbpn = bpn + s = rd.getVar('S', False) + src_uri = rd.getVar('SRC_URI', False) + pv = rd.getVar('PV') + + # Correct variable values that refer to the upstream source - these + # values must stay the same, so if the name/version are changing then + # we need to fix them up + new_s = s + new_src_uri = src_uri + if newbpn != bpn: + # ${PN} here is technically almost always incorrect, but people do use it + new_s = new_s.replace('${BPN}', bpn) + new_s = new_s.replace('${PN}', bpn) + new_s = new_s.replace('${BP}', '%s-${PV}' % bpn) + new_src_uri = new_src_uri.replace('${BPN}', bpn) + new_src_uri = new_src_uri.replace('${PN}', bpn) + new_src_uri = new_src_uri.replace('${BP}', '%s-${PV}' % bpn) + if args.version and origfnver == pv: + new_s = new_s.replace('${PV}', pv) + new_s = new_s.replace('${BP}', '${BPN}-%s' % pv) + new_src_uri = new_src_uri.replace('${PV}', pv) + new_src_uri = new_src_uri.replace('${BP}', '${BPN}-%s' % pv) + patchfields = {} + if new_s != s: + patchfields['S'] = new_s + if new_src_uri != src_uri: + patchfields['SRC_URI'] = new_src_uri + if patchfields: + recipefilemd5 = bb.utils.md5_file(recipefile) + oe.recipeutils.patch_recipe(rd, recipefile, patchfields) + newrecipefilemd5 = bb.utils.md5_file(recipefile) + finally: + tinfoil.shutdown() - _add_md5(config, pn, appendfile) + if args.version: + newver = args.version + else: + newver = origfnver - logger.info('Recipe %s now set up to build from %s' % (pn, srctree)) + if newver: + newappend = '%s_%s.bbappend' % (newname, newver) + newfile = '%s_%s.bb' % (newname, newver) + else: + newappend = '%s.bbappend' % newname + newfile = '%s.bb' % newname + + oldrecipedir = os.path.dirname(recipefile) + newrecipedir = os.path.join(config.workspace_path, 'recipes', newname) + if oldrecipedir != newrecipedir: + bb.utils.mkdirhier(newrecipedir) + + newappend = os.path.join(os.path.dirname(append), newappend) + newfile = os.path.join(newrecipedir, newfile) + + # Rename bbappend + logger.info('Renaming %s to %s' % (append, newappend)) + os.rename(append, newappend) + # Rename recipe file + logger.info('Renaming %s to %s' % (recipefile, newfile)) + os.rename(recipefile, newfile) + + # Rename source tree if it's the default path + appendmd5 = None + if not args.no_srctree: + srctree = workspace[args.recipename]['srctree'] + if os.path.abspath(srctree) == os.path.join(config.workspace_path, 'sources', args.recipename): + newsrctree = os.path.join(config.workspace_path, 'sources', newname) + logger.info('Renaming %s to %s' % (srctree, newsrctree)) + shutil.move(srctree, newsrctree) + # Correct any references (basically EXTERNALSRC*) in the .bbappend + appendmd5 = bb.utils.md5_file(newappend) + appendlines = [] + with open(newappend, 'r') as f: + for line in f: + appendlines.append(line) + with open(newappend, 'w') as f: + for line in appendlines: + if srctree in line: + line = line.replace(srctree, newsrctree) + f.write(line) + newappendmd5 = bb.utils.md5_file(newappend) + + bpndir = None + newbpndir = None + if newbpn != bpn: + bpndir = os.path.join(oldrecipedir, bpn) + if os.path.exists(bpndir): + newbpndir = os.path.join(newrecipedir, newbpn) + logger.info('Renaming %s to %s' % (bpndir, newbpndir)) + shutil.move(bpndir, newbpndir) + + bpdir = None + newbpdir = None + if newver != origfnver or newbpn != bpn: + bpdir = os.path.join(oldrecipedir, bp) + if os.path.exists(bpdir): + newbpdir = os.path.join(newrecipedir, '%s-%s' % (newbpn, newver)) + logger.info('Renaming %s to %s' % (bpdir, newbpdir)) + shutil.move(bpdir, newbpdir) + + if oldrecipedir != newrecipedir: + # Move any stray files and delete the old recipe directory + for entry in os.listdir(oldrecipedir): + oldpath = os.path.join(oldrecipedir, entry) + newpath = os.path.join(newrecipedir, entry) + logger.info('Renaming %s to %s' % (oldpath, newpath)) + shutil.move(oldpath, newpath) + os.rmdir(oldrecipedir) + + # Now take care of entries in .devtool_md5 + md5entries = [] + with open(os.path.join(config.workspace_path, '.devtool_md5'), 'r') as f: + for line in f: + md5entries.append(line) + if bpndir and newbpndir: + relbpndir = os.path.relpath(bpndir, config.workspace_path) + '/' + else: + relbpndir = None + if bpdir and newbpdir: + relbpdir = os.path.relpath(bpdir, config.workspace_path) + '/' + else: + relbpdir = None + + with open(os.path.join(config.workspace_path, '.devtool_md5'), 'w') as f: + for entry in md5entries: + splitentry = entry.rstrip().split('|') + if len(splitentry) > 2: + if splitentry[0] == args.recipename: + splitentry[0] = newname + if splitentry[1] == os.path.relpath(append, config.workspace_path): + splitentry[1] = os.path.relpath(newappend, config.workspace_path) + if appendmd5 and splitentry[2] == appendmd5: + splitentry[2] = newappendmd5 + elif splitentry[1] == os.path.relpath(recipefile, config.workspace_path): + splitentry[1] = os.path.relpath(newfile, config.workspace_path) + if recipefilemd5 and splitentry[2] == recipefilemd5: + splitentry[2] = newrecipefilemd5 + elif relbpndir and splitentry[1].startswith(relbpndir): + splitentry[1] = os.path.relpath(os.path.join(newbpndir, splitentry[1][len(relbpndir):]), config.workspace_path) + elif relbpdir and splitentry[1].startswith(relbpdir): + splitentry[1] = os.path.relpath(os.path.join(newbpdir, splitentry[1][len(relbpdir):]), config.workspace_path) + entry = '|'.join(splitentry) + '\n' + f.write(entry) return 0 -def _get_patchset_revs(args, srctree, recipe_path): + +def _get_patchset_revs(srctree, recipe_path, initial_rev=None): """Get initial and update rev of a recipe. These are the start point of the whole patchset and start point for the patches to be re-generated/updated. """ import bb - if args.initial_rev: - return args.initial_rev, args.initial_rev - - # Parse initial rev from recipe + # Parse initial rev from recipe if not specified commits = [] - initial_rev = None with open(recipe_path, 'r') as f: for line in f: if line.startswith('# initial_rev:'): - initial_rev = line.split(':')[-1].strip() + if not initial_rev: + initial_rev = line.split(':')[-1].strip() elif line.startswith('# commit:'): commits.append(line.split(':')[-1].strip()) @@ -808,7 +1077,7 @@ def _get_patchset_revs(args, srctree, recipe_path): 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))): + for i in range(min(len(commits), len(newcommits))): if newcommits[i] == commits[i]: update_rev = commits[i] @@ -834,7 +1103,7 @@ def _remove_file_entries(srcuri, filelist): entries = [] for fname in filelist: basename = os.path.basename(fname) - for i in xrange(len(srcuri)): + for i in range(len(srcuri)): if (srcuri[i].startswith('file://') and os.path.basename(srcuri[i].split(';')[0]) == basename): entries.append(srcuri[i]) @@ -843,10 +1112,19 @@ def _remove_file_entries(srcuri, filelist): break return entries, remaining -def _remove_source_files(args, files, destpath): +def _replace_srcuri_entry(srcuri, filename, newentry): + """Replace entry corresponding to specified file with a new entry""" + basename = os.path.basename(filename) + for i in range(len(srcuri)): + if os.path.basename(srcuri[i].split(';')[0]) == basename: + srcuri.pop(i) + srcuri.insert(i, newentry) + break + +def _remove_source_files(append, files, destpath): """Unlink existing patch files""" for path in files: - if args.append: + if append: if not destpath: raise Exception('destpath should be set here') path = os.path.join(destpath, os.path.basename(path)) @@ -867,7 +1145,7 @@ def _remove_source_files(args, files, destpath): raise -def _export_patches(srctree, rd, start_rev, destdir): +def _export_patches(srctree, rd, start_rev, destdir, changed_revs=None): """Export patches from srctree to given location. Returns three-tuple of dicts: 1. updated - patches that already exist in SRCURI @@ -896,22 +1174,75 @@ def _export_patches(srctree, rd, start_rev, destdir): # revision This does assume that people are using unique shortlog # values, but they ought to be anyway... new_basename = seqpatch_re.match(new_patch).group(2) - found = False + match_name = None for old_patch in existing_patches: old_basename = seqpatch_re.match(old_patch).group(2) - if new_basename == old_basename: - updated[new_patch] = existing_patches.pop(old_patch) - found = True - # Rename patch files - if new_patch != old_patch: - os.rename(os.path.join(destdir, new_patch), - os.path.join(destdir, old_patch)) + old_basename_splitext = os.path.splitext(old_basename) + if old_basename.endswith(('.gz', '.bz2', '.Z')) and old_basename_splitext[0] == new_basename: + old_patch_noext = os.path.splitext(old_patch)[0] + match_name = old_patch_noext break - if not found: + elif new_basename == old_basename: + match_name = old_patch + break + if match_name: + # Rename patch files + if new_patch != match_name: + os.rename(os.path.join(destdir, new_patch), + os.path.join(destdir, match_name)) + # Need to pop it off the list now before checking changed_revs + oldpath = existing_patches.pop(old_patch) + if changed_revs is not None: + # Avoid updating patches that have not actually changed + with open(os.path.join(destdir, match_name), 'r') as f: + firstlineitems = f.readline().split() + # Looking for "From <hash>" line + if len(firstlineitems) > 1 and len(firstlineitems[1]) == 40: + if not firstlineitems[1] in changed_revs: + continue + # Recompress if necessary + if oldpath.endswith(('.gz', '.Z')): + bb.process.run(['gzip', match_name], cwd=destdir) + if oldpath.endswith('.gz'): + match_name += '.gz' + else: + match_name += '.Z' + elif oldpath.endswith('.bz2'): + bb.process.run(['bzip2', match_name], cwd=destdir) + match_name += '.bz2' + updated[match_name] = oldpath + else: added[new_patch] = None return (updated, added, existing_patches) +def _create_kconfig_diff(srctree, rd, outfile): + """Create a kconfig fragment""" + # Only update config fragment if both config files exist + orig_config = os.path.join(srctree, '.config.baseline') + new_config = os.path.join(srctree, '.config.new') + if os.path.exists(orig_config) and os.path.exists(new_config): + cmd = ['diff', '--new-line-format=%L', '--old-line-format=', + '--unchanged-line-format=', orig_config, new_config] + pipe = subprocess.Popen(cmd, stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + stdout, stderr = pipe.communicate() + if pipe.returncode == 1: + logger.info("Updating config fragment %s" % outfile) + with open(outfile, 'wb') as fobj: + fobj.write(stdout) + elif pipe.returncode == 0: + logger.info("Would remove config fragment %s" % outfile) + if os.path.exists(outfile): + # Remove fragment file in case of empty diff + logger.info("Removing config fragment %s" % outfile) + os.unlink(outfile) + else: + raise bb.process.ExecutionError(cmd, pipe.returncode, stdout, stderr) + return True + return False + + def _export_local_files(srctree, rd, destdir): """Copy local files from srctree to given location. Returns three-tuple of dicts: @@ -932,6 +1263,7 @@ def _export_local_files(srctree, rd, destdir): updated = OrderedDict() added = OrderedDict() removed = OrderedDict() + local_files_dir = os.path.join(srctree, 'oe-local-files') git_files = _git_ls_tree(srctree) if 'oe-local-files' in git_files: # If tracked by Git, take the files from srctree HEAD. First get @@ -941,29 +1273,82 @@ def _export_local_files(srctree, rd, destdir): bb.process.run(['git', 'checkout', tree, '--', '.'], cwd=srctree, env=dict(os.environ, GIT_WORK_TREE=destdir, GIT_INDEX_FILE=tmp_index)) - new_set = _git_ls_tree(srctree, tree, True).keys() - elif os.path.isdir(os.path.join(srctree, 'oe-local-files')): + new_set = list(_git_ls_tree(srctree, tree, True).keys()) + elif os.path.isdir(local_files_dir): # If not tracked by Git, just copy from working copy new_set = _ls_tree(os.path.join(srctree, 'oe-local-files')) bb.process.run(['cp', '-ax', os.path.join(srctree, 'oe-local-files', '.'), destdir]) + else: + new_set = [] + + # Special handling for kernel config + if bb.data.inherits_class('kernel-yocto', rd): + fragment_fn = 'devtool-fragment.cfg' + fragment_path = os.path.join(destdir, fragment_fn) + if _create_kconfig_diff(srctree, rd, fragment_path): + if os.path.exists(fragment_path): + if fragment_fn not in new_set: + new_set.append(fragment_fn) + # Copy fragment to local-files + if os.path.isdir(local_files_dir): + shutil.copy2(fragment_path, local_files_dir) + else: + if fragment_fn in new_set: + new_set.remove(fragment_fn) + # Remove fragment from local-files + if os.path.exists(os.path.join(local_files_dir, fragment_fn)): + os.unlink(os.path.join(local_files_dir, fragment_fn)) + if new_set is not None: for fname in new_set: if fname in existing_files: - updated[fname] = existing_files.pop(fname) + origpath = existing_files.pop(fname) + workpath = os.path.join(local_files_dir, fname) + if not filecmp.cmp(origpath, workpath): + updated[fname] = origpath elif fname != '.gitignore': added[fname] = None + workdir = rd.getVar('WORKDIR') + s = rd.getVar('S') + if not s.endswith(os.sep): + s += os.sep + + if workdir != s: + # Handle files where subdir= was specified + for fname in list(existing_files.keys()): + # FIXME handle both subdir starting with BP and not? + fworkpath = os.path.join(workdir, fname) + if fworkpath.startswith(s): + fpath = os.path.join(srctree, os.path.relpath(fworkpath, s)) + if os.path.exists(fpath): + origpath = existing_files.pop(fname) + if not filecmp.cmp(origpath, fpath): + updated[fpath] = origpath + removed = existing_files return (updated, added, removed) -def _update_recipe_srcrev(args, srctree, rd, config_data): +def _determine_files_dir(rd): + """Determine the appropriate files directory for a recipe""" + recipedir = rd.getVar('FILE_DIRNAME') + for entry in rd.getVar('FILESPATH').split(':'): + relpth = os.path.relpath(entry, recipedir) + if not os.sep in relpth: + # One (or zero) levels below only, so we don't put anything in machine-specific directories + if os.path.isdir(entry): + return entry + return os.path.join(recipedir, rd.getVar('BPN')) + + +def _update_recipe_srcrev(srctree, rd, appendlayerdir, wildcard_version, no_remove): """Implement the 'srcrev' mode of update-recipe""" import bb import oe.recipeutils - recipefile = rd.getVar('FILE', True) + recipefile = rd.getVar('FILE') logger.info('Updating SRCREV in recipe %s' % os.path.basename(recipefile)) # Get HEAD revision @@ -987,7 +1372,7 @@ def _update_recipe_srcrev(args, srctree, rd, config_data): try: local_files_dir = tempfile.mkdtemp(dir=tempdir) upd_f, new_f, del_f = _export_local_files(srctree, rd, local_files_dir) - if not args.no_remove: + if not no_remove: # Find list of existing patches in recipe file patches_dir = tempfile.mkdtemp(dir=tempdir) old_srcrev = (rd.getVar('SRCREV', False) or '') @@ -995,29 +1380,33 @@ def _update_recipe_srcrev(args, srctree, rd, config_data): patches_dir) # Remove deleted local files and "overlapping" patches - remove_files = del_f.values() + upd_p.values() + remove_files = list(del_f.values()) + list(upd_p.values()) if remove_files: removedentries = _remove_file_entries(srcuri, remove_files)[0] update_srcuri = True - if args.append: + if appendlayerdir: files = dict((os.path.join(local_files_dir, key), val) for - key, val in upd_f.items() + new_f.items()) + key, val in list(upd_f.items()) + list(new_f.items())) removevalues = {} if update_srcuri: removevalues = {'SRC_URI': removedentries} patchfields['SRC_URI'] = '\\\n '.join(srcuri) _, destpath = oe.recipeutils.bbappend_recipe( - rd, args.append, files, wildcardver=args.wildcard_version, + rd, appendlayerdir, files, wildcardver=wildcard_version, extralines=patchfields, removevalues=removevalues) else: - files_dir = os.path.join(os.path.dirname(recipefile), - rd.getVar('BPN', True)) - for basepath, path in upd_f.iteritems(): + files_dir = _determine_files_dir(rd) + for basepath, path in upd_f.items(): logger.info('Updating file %s' % basepath) - _move_file(os.path.join(local_files_dir, basepath), path) + if os.path.isabs(basepath): + # Original file (probably with subdir pointing inside source tree) + # so we do not want to move it, just copy + _copy_file(basepath, path) + else: + _move_file(os.path.join(local_files_dir, basepath), path) update_srcuri= True - for basepath, path in new_f.iteritems(): + for basepath, path in new_f.items(): logger.info('Adding new file %s' % basepath) _move_file(os.path.join(local_files_dir, basepath), os.path.join(files_dir, basepath)) @@ -1033,51 +1422,56 @@ def _update_recipe_srcrev(args, srctree, rd, config_data): 'point to a git repository where you have pushed your ' 'changes') - _remove_source_files(args, remove_files, destpath) + _remove_source_files(appendlayerdir, remove_files, destpath) + return True -def _update_recipe_patch(args, config, workspace, srctree, rd, config_data): +def _update_recipe_patch(recipename, workspace, srctree, rd, appendlayerdir, wildcard_version, no_remove, initial_rev): """Implement the 'patch' mode of update-recipe""" import bb import oe.recipeutils - recipefile = rd.getVar('FILE', True) - append = workspace[args.recipename]['bbappend'] + recipefile = rd.getVar('FILE') + append = workspace[recipename]['bbappend'] if not os.path.exists(append): raise DevtoolError('unable to find workspace bbappend for recipe %s' % - args.recipename) + recipename) - initial_rev, update_rev, changed_revs = _get_patchset_revs(args, srctree, append) + initial_rev, update_rev, changed_revs = _get_patchset_revs(srctree, append, initial_rev) if not initial_rev: raise DevtoolError('Unable to find initial revision - please specify ' 'it with --initial-rev') + dl_dir = rd.getVar('DL_DIR') + if not dl_dir.endswith('/'): + dl_dir += '/' + tempdir = tempfile.mkdtemp(prefix='devtool') try: local_files_dir = tempfile.mkdtemp(dir=tempdir) upd_f, new_f, del_f = _export_local_files(srctree, rd, local_files_dir) remove_files = [] - if not args.no_remove: + if not no_remove: # Get all patches from source tree and check if any should be removed all_patches_dir = tempfile.mkdtemp(dir=tempdir) upd_p, new_p, del_p = _export_patches(srctree, rd, initial_rev, all_patches_dir) # Remove deleted local files and patches - remove_files = del_f.values() + del_p.values() + remove_files = list(del_f.values()) + list(del_p.values()) # Get updated patches from source tree patches_dir = tempfile.mkdtemp(dir=tempdir) upd_p, new_p, del_p = _export_patches(srctree, rd, update_rev, - patches_dir) + patches_dir, changed_revs) updatefiles = False updaterecipe = False destpath = None srcuri = (rd.getVar('SRC_URI', False) or '').split() - if args.append: + if appendlayerdir: files = dict((os.path.join(local_files_dir, key), val) for - key, val in upd_f.items() + new_f.items()) + key, val in list(upd_f.items()) + list(new_f.items())) files.update(dict((os.path.join(patches_dir, key), val) for - key, val in upd_p.items() + new_p.items())) + key, val in list(upd_p.items()) + list(new_p.items()))) if files or remove_files: removevalues = None if remove_files: @@ -1088,38 +1482,45 @@ def _update_recipe_patch(args, config, workspace, srctree, rd, config_data): item in remaining] removevalues = {'SRC_URI': removedentries + remaining} _, destpath = oe.recipeutils.bbappend_recipe( - rd, args.append, files, + rd, appendlayerdir, files, + wildcardver=wildcard_version, removevalues=removevalues) else: logger.info('No patches or local source files needed updating') else: # Update existing files - for basepath, path in upd_f.iteritems(): + files_dir = _determine_files_dir(rd) + for basepath, path in upd_f.items(): logger.info('Updating file %s' % basepath) - _move_file(os.path.join(local_files_dir, basepath), path) + if os.path.isabs(basepath): + # Original file (probably with subdir pointing inside source tree) + # so we do not want to move it, just copy + _copy_file(basepath, path) + else: + _move_file(os.path.join(local_files_dir, basepath), path) updatefiles = True - for basepath, path in upd_p.iteritems(): + for basepath, path in upd_p.items(): patchfn = os.path.join(patches_dir, basepath) - if changed_revs is not None: - # Avoid updating patches that have not actually changed - with open(patchfn, 'r') as f: - firstlineitems = f.readline().split() - if len(firstlineitems) > 1 and len(firstlineitems[1]) == 40: - if not firstlineitems[1] in changed_revs: - continue - logger.info('Updating patch %s' % basepath) + if os.path.dirname(path) + '/' == dl_dir: + # This is a a downloaded patch file - we now need to + # replace the entry in SRC_URI with our local version + logger.info('Replacing remote patch %s with updated local version' % basepath) + path = os.path.join(files_dir, basepath) + _replace_srcuri_entry(srcuri, basepath, 'file://%s' % basepath) + updaterecipe = True + else: + logger.info('Updating patch %s' % basepath) + logger.debug('Moving new patch %s to %s' % (patchfn, path)) _move_file(patchfn, path) updatefiles = True # Add any new files - files_dir = os.path.join(os.path.dirname(recipefile), - rd.getVar('BPN', True)) - for basepath, path in new_f.iteritems(): + for basepath, path in new_f.items(): logger.info('Adding new file %s' % basepath) _move_file(os.path.join(local_files_dir, basepath), os.path.join(files_dir, basepath)) srcuri.append('file://%s' % basepath) updaterecipe = True - for basepath, path in new_p.iteritems(): + for basepath, path in new_p.items(): logger.info('Adding new patch %s' % basepath) _move_file(os.path.join(patches_dir, basepath), os.path.join(files_dir, basepath)) @@ -1135,10 +1536,12 @@ def _update_recipe_patch(args, config, workspace, srctree, rd, config_data): elif not updatefiles: # Neither patches nor recipe were updated logger.info('No patches or files need updating') + return False finally: shutil.rmtree(tempdir) - _remove_source_files(args, remove_files, destpath) + _remove_source_files(appendlayerdir, remove_files, destpath) + return True def _guess_recipe_update_mode(srctree, rdata): """Guess the recipe update mode to use""" @@ -1162,6 +1565,19 @@ def _guess_recipe_update_mode(srctree, rdata): return 'patch' +def _update_recipe(recipename, workspace, rd, mode, appendlayerdir, wildcard_version, no_remove, initial_rev): + srctree = workspace[recipename]['srctree'] + if mode == 'auto': + mode = _guess_recipe_update_mode(srctree, rd) + + if mode == 'srcrev': + updated = _update_recipe_srcrev(srctree, rd, appendlayerdir, wildcard_version, no_remove) + elif mode == 'patch': + updated = _update_recipe_patch(recipename, workspace, srctree, rd, appendlayerdir, wildcard_version, no_remove, initial_rev) + else: + raise DevtoolError('update_recipe: invalid mode %s' % mode) + return updated + def update_recipe(args, config, basepath, workspace): """Entry point for the devtool 'update-recipe' subcommand""" check_workspace_recipe(workspace, args.recipename) @@ -1175,27 +1591,20 @@ def update_recipe(args, config, basepath, workspace): 'destination layer "%s"' % args.append) tinfoil = setup_tinfoil(basepath=basepath, tracking=True) + try: - rd = parse_recipe(config, tinfoil, args.recipename, True) - if not rd: - return 1 - - srctree = workspace[args.recipename]['srctree'] - if args.mode == 'auto': - mode = _guess_recipe_update_mode(srctree, rd) - else: - mode = args.mode + rd = parse_recipe(config, tinfoil, args.recipename, True) + if not rd: + return 1 - if mode == 'srcrev': - _update_recipe_srcrev(args, srctree, rd, tinfoil.config_data) - elif mode == 'patch': - _update_recipe_patch(args, config, workspace, srctree, rd, tinfoil.config_data) - else: - raise DevtoolError('update_recipe: invalid mode %s' % mode) + updated = _update_recipe(args.recipename, workspace, rd, args.mode, args.append, args.wildcard_version, args.no_remove, args.initial_rev) - rf = rd.getVar('FILE', True) - if rf.startswith(config.workspace_path): - logger.warn('Recipe file %s has been updated but is inside the workspace - you will need to move it (and any associated files next to it) out to the desired layer before using "devtool reset" in order to keep any changes' % rf) + if updated: + rf = rd.getVar('FILE') + if rf.startswith(config.workspace_path): + logger.warn('Recipe file %s has been updated but is inside the workspace - you will need to move it (and any associated files next to it) out to the desired layer before using "devtool reset" in order to keep any changes' % rf) + finally: + tinfoil.shutdown() return 0 @@ -1203,7 +1612,7 @@ def update_recipe(args, config, basepath, workspace): def status(args, config, basepath, workspace): """Entry point for the devtool 'status' subcommand""" if workspace: - for recipe, value in workspace.iteritems(): + for recipe, value in workspace.items(): recipefile = value['recipefile'] if recipefile: recipestr = ' (%s)' % recipefile @@ -1215,23 +1624,10 @@ def status(args, config, basepath, workspace): return 0 -def reset(args, config, basepath, workspace): - """Entry point for the devtool 'reset' subcommand""" - import bb - if args.recipename: - if args.all: - raise DevtoolError("Recipe cannot be specified if -a/--all is used") - else: - check_workspace_recipe(workspace, args.recipename, checksrc=False) - elif not args.all: - raise DevtoolError("Recipe must be specified, or specify -a/--all to " - "reset all recipes") - if args.all: - recipes = workspace.keys() - else: - recipes = [args.recipename] +def _reset(recipes, no_clean, config, basepath, workspace): + """Reset one or more recipes""" - if recipes and not args.no_clean: + if recipes and not no_clean: if len(recipes) == 1: logger.info('Cleaning sysroot for recipe %s...' % recipes[0]) else: @@ -1243,7 +1639,7 @@ def reset(args, config, basepath, workspace): for recipe in recipes: targets.append(recipe) recipefile = workspace[recipe]['recipefile'] - if recipefile: + if recipefile and os.path.exists(recipefile): targets.extend(get_bbclassextend_targets(recipefile, recipe)) try: exec_build_env_command(config.init_path, basepath, 'bitbake -c clean %s' % ' '.join(targets)) @@ -1256,7 +1652,7 @@ def reset(args, config, basepath, workspace): for pn in recipes: _check_preserve(config, pn) - preservepath = os.path.join(config.workspace_path, 'attic', pn) + preservepath = os.path.join(config.workspace_path, 'attic', pn, pn) def preservedir(origdir): if os.path.exists(origdir): for root, dirs, files in os.walk(origdir): @@ -1265,7 +1661,7 @@ def reset(args, config, basepath, workspace): _move_file(os.path.join(origdir, fn), os.path.join(preservepath, fn)) for dn in dirs: - os.rmdir(os.path.join(root, dn)) + preservedir(os.path.join(root, dn)) os.rmdir(origdir) preservedir(os.path.join(config.workspace_path, 'recipes', pn)) @@ -1283,6 +1679,133 @@ def reset(args, config, basepath, workspace): # This is unlikely, but if it's empty we can just remove it os.rmdir(srctree) + +def reset(args, config, basepath, workspace): + """Entry point for the devtool 'reset' subcommand""" + import bb + if args.recipename: + if args.all: + raise DevtoolError("Recipe cannot be specified if -a/--all is used") + else: + for recipe in args.recipename: + check_workspace_recipe(workspace, recipe, checksrc=False) + elif not args.all: + raise DevtoolError("Recipe must be specified, or specify -a/--all to " + "reset all recipes") + if args.all: + recipes = list(workspace.keys()) + else: + recipes = args.recipename + + _reset(recipes, args.no_clean, config, basepath, workspace) + + return 0 + + +def _get_layer(layername, d): + """Determine the base layer path for the specified layer name/path""" + layerdirs = d.getVar('BBLAYERS').split() + layers = {os.path.basename(p): p for p in layerdirs} + # Provide some shortcuts + if layername.lower() in ['oe-core', 'openembedded-core']: + layerdir = layers.get('meta', None) + else: + layerdir = layers.get(layername, None) + if layerdir: + layerdir = os.path.abspath(layerdir) + return layerdir or layername + +def finish(args, config, basepath, workspace): + """Entry point for the devtool 'finish' subcommand""" + import bb + import oe.recipeutils + + check_workspace_recipe(workspace, args.recipename) + + no_clean = False + tinfoil = setup_tinfoil(basepath=basepath, tracking=True) + try: + rd = parse_recipe(config, tinfoil, args.recipename, True) + if not rd: + return 1 + + destlayerdir = _get_layer(args.destination, tinfoil.config_data) + origlayerdir = oe.recipeutils.find_layerdir(rd.getVar('FILE')) + + if not os.path.isdir(destlayerdir): + raise DevtoolError('Unable to find layer or directory matching "%s"' % args.destination) + + if os.path.abspath(destlayerdir) == config.workspace_path: + raise DevtoolError('"%s" specifies the workspace layer - that is not a valid destination' % args.destination) + + # If it's an upgrade, grab the original path + origpath = None + origfilelist = None + append = workspace[args.recipename]['bbappend'] + with open(append, 'r') as f: + for line in f: + if line.startswith('# original_path:'): + origpath = line.split(':')[1].strip() + elif line.startswith('# original_files:'): + origfilelist = line.split(':')[1].split() + + if origlayerdir == config.workspace_path: + # Recipe file itself is in workspace, update it there first + appendlayerdir = None + origrelpath = None + if origpath: + origlayerpath = oe.recipeutils.find_layerdir(origpath) + if origlayerpath: + origrelpath = os.path.relpath(origpath, origlayerpath) + destpath = oe.recipeutils.get_bbfile_path(rd, destlayerdir, origrelpath) + if not destpath: + raise DevtoolError("Unable to determine destination layer path - check that %s specifies an actual layer and %s/conf/layer.conf specifies BBFILES. You may also need to specify a more complete path." % (args.destination, destlayerdir)) + # Warn if the layer isn't in bblayers.conf (the code to create a bbappend will do this in other cases) + layerdirs = [os.path.abspath(layerdir) for layerdir in rd.getVar('BBLAYERS').split()] + if not os.path.abspath(destlayerdir) in layerdirs: + bb.warn('Specified destination layer is not currently enabled in bblayers.conf, so the %s recipe will now be unavailable in your current configuration until you add the layer there' % args.recipename) + + elif destlayerdir == origlayerdir: + # Same layer, update the original recipe + appendlayerdir = None + destpath = None + else: + # Create/update a bbappend in the specified layer + appendlayerdir = destlayerdir + destpath = None + + # Remove any old files in the case of an upgrade + if origpath and origfilelist and oe.recipeutils.find_layerdir(origpath) == oe.recipeutils.find_layerdir(destlayerdir): + for fn in origfilelist: + fnp = os.path.join(origpath, fn) + try: + os.remove(fnp) + except FileNotFoundError: + pass + + # Actually update the recipe / bbappend + _update_recipe(args.recipename, workspace, rd, args.mode, appendlayerdir, wildcard_version=True, no_remove=False, initial_rev=args.initial_rev) + + if origlayerdir == config.workspace_path and destpath: + # Recipe file itself is in the workspace - need to move it and any + # associated files to the specified layer + no_clean = True + logger.info('Moving recipe file to %s' % destpath) + recipedir = os.path.dirname(rd.getVar('FILE')) + for root, _, files in os.walk(recipedir): + for fn in files: + srcpath = os.path.join(root, fn) + relpth = os.path.relpath(os.path.dirname(srcpath), recipedir) + destdir = os.path.abspath(os.path.join(destpath, relpth)) + bb.utils.mkdirhier(destdir) + shutil.move(srcpath, os.path.join(destdir, fn)) + + finally: + tinfoil.shutdown() + + # Everything else has succeeded, we can now reset + _reset([args.recipename], no_clean=no_clean, config=config, basepath=basepath, workspace=workspace) + return 0 @@ -1299,7 +1822,8 @@ def register_commands(subparsers, context): defsrctree = get_default_srctree(context.config) parser_add = subparsers.add_parser('add', help='Add a new recipe', - description='Adds a new recipe to the workspace to build a specified source tree. Can optionally fetch a remote URI and unpack it to create the source tree.') + description='Adds a new recipe to the workspace to build a specified source tree. Can optionally fetch a remote URI and unpack it to create the source tree.', + group='starting', order=100) parser_add.add_argument('recipename', nargs='?', help='Name for new recipe to add (just name - no version, path or extension). If not specified, will attempt to auto-detect it.') parser_add.add_argument('srctree', nargs='?', help='Path to external source tree. If not specified, a subdirectory of %s will be used.' % defsrctree) parser_add.add_argument('fetchuri', nargs='?', help='Fetch the specified URI and extract it to create the source tree') @@ -1307,15 +1831,18 @@ def register_commands(subparsers, context): group.add_argument('--same-dir', '-s', help='Build in same directory as source', action="store_true") group.add_argument('--no-same-dir', help='Force build in a separate build directory', action="store_true") parser_add.add_argument('--fetch', '-f', help='Fetch the specified URI and extract it to create the source tree (deprecated - pass as positional argument instead)', metavar='URI') + parser_add.add_argument('--fetch-dev', help='For npm, also fetch devDependencies', action="store_true") parser_add.add_argument('--version', '-V', help='Version to use within recipe (PV)') parser_add.add_argument('--no-git', '-g', help='If fetching source, do not set up source tree as a git repository', action="store_true") + parser_add.add_argument('--autorev', '-a', help='When fetching from a git repository, set SRCREV in the recipe to a floating revision instead of fixed', action="store_true") parser_add.add_argument('--binary', '-b', help='Treat the source tree as something that should be installed verbatim (no compilation, same directory structure). Useful with binary packages e.g. RPMs.', action='store_true') parser_add.add_argument('--also-native', help='Also add native variant (i.e. support building recipe for the build host as well as the target machine)', action='store_true') parser_add.add_argument('--src-subdir', help='Specify subdirectory within source tree to use', metavar='SUBDIR') - parser_add.set_defaults(func=add) + parser_add.set_defaults(func=add, fixed_setup=context.fixed_setup) parser_modify = subparsers.add_parser('modify', help='Modify the source for an existing recipe', - description='Enables modifying the source for an existing recipe. You can either provide your own pre-prepared source tree, or specify -x/--extract to extract the source being fetched by the recipe.') + description='Sets up the build environment to modify the source for an existing recipe. The default behaviour is to extract the source being fetched by the recipe into a git tree so you can work on it; alternatively if you already have your own pre-prepared source tree you can specify -n/--no-extract.', + group='starting', order=90) parser_modify.add_argument('recipename', help='Name of existing recipe to edit (just name - no version, path or extension)') parser_modify.add_argument('srctree', nargs='?', help='Path to external source tree. If not specified, a subdirectory of %s will be used.' % defsrctree) parser_modify.add_argument('--wildcard', '-w', action="store_true", help='Use wildcard for unversioned bbappend') @@ -1325,11 +1852,13 @@ def register_commands(subparsers, context): group = parser_modify.add_mutually_exclusive_group() group.add_argument('--same-dir', '-s', help='Build in same directory as source', action="store_true") group.add_argument('--no-same-dir', help='Force build in a separate build directory', action="store_true") - parser_modify.add_argument('--branch', '-b', default="devtool", help='Name for development branch to checkout (only when using -x) (default "%(default)s")') + parser_modify.add_argument('--branch', '-b', default="devtool", help='Name for development branch to checkout (when not using -n/--no-extract) (default "%(default)s")') + parser_modify.add_argument('--keep-temp', help='Keep temporary directory (for debugging)', action="store_true") parser_modify.set_defaults(func=modify) parser_extract = subparsers.add_parser('extract', help='Extract the source for an existing recipe', - description='Extracts the source for an existing recipe') + description='Extracts the source for an existing recipe', + group='advanced') parser_extract.add_argument('recipename', help='Name of recipe to extract the source for') parser_extract.add_argument('srctree', help='Path to where to extract the source tree') parser_extract.add_argument('--branch', '-b', default="devtool", help='Name for development branch to checkout (default "%(default)s")') @@ -1338,15 +1867,26 @@ def register_commands(subparsers, context): parser_sync = subparsers.add_parser('sync', help='Synchronize the source tree for an existing recipe', description='Synchronize the previously extracted source tree for an existing recipe', - formatter_class=argparse.ArgumentDefaultsHelpFormatter) + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + group='advanced') parser_sync.add_argument('recipename', help='Name of recipe to sync the source for') parser_sync.add_argument('srctree', help='Path to the source tree') parser_sync.add_argument('--branch', '-b', default="devtool", help='Name for development branch to checkout') parser_sync.add_argument('--keep-temp', action="store_true", help='Keep temporary directory (for debugging)') parser_sync.set_defaults(func=sync) + parser_rename = subparsers.add_parser('rename', help='Rename a recipe file in the workspace', + description='Renames the recipe file for a recipe in the workspace, changing the name or version part or both, ensuring that all references within the workspace are updated at the same time. Only works when the recipe file itself is in the workspace, e.g. after devtool add. Particularly useful when devtool add did not automatically determine the correct name.', + group='working', order=10) + parser_rename.add_argument('recipename', help='Current name of recipe to rename') + parser_rename.add_argument('newname', nargs='?', help='New name for recipe (optional, not needed if you only want to change the version)') + parser_rename.add_argument('--version', '-V', help='Change the version (NOTE: this does not change the version fetched by the recipe, just the version in the recipe file name)') + parser_rename.add_argument('--no-srctree', '-s', action='store_true', help='Do not rename the source tree directory (if the default source tree path has been used) - keeping the old name may be desirable if there are internal/other external references to this path') + parser_rename.set_defaults(func=rename) + parser_update_recipe = 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). Note that these changes need to have been committed to the git repository in order to be recognised.') + description='Applies changes from external source tree to a recipe (updating/adding/removing patches as necessary, or by updating SRCREV). Note that these changes need to have been committed to the git repository in order to be recognised.', + group='working', order=-90) parser_update_recipe.add_argument('recipename', help='Name of recipe to update') parser_update_recipe.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_update_recipe.add_argument('--initial-rev', help='Override starting revision for patches') @@ -1356,12 +1896,23 @@ def register_commands(subparsers, context): parser_update_recipe.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') + description='Lists recipes currently in your workspace and the paths to their respective external source trees', + group='info', order=100) parser_status.set_defaults(func=status) parser_reset = subparsers.add_parser('reset', help='Remove a recipe from your workspace', - description='Removes the specified recipe from your workspace (resetting its state)') - parser_reset.add_argument('recipename', nargs='?', help='Recipe to reset') + description='Removes the specified recipe(s) from your workspace (resetting its state back to that defined by the metadata).', + group='working', order=-100) + parser_reset.add_argument('recipename', nargs='*', help='Recipe to reset') parser_reset.add_argument('--all', '-a', action="store_true", help='Reset all recipes (clear workspace)') 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) + + parser_finish = subparsers.add_parser('finish', help='Finish working on a recipe in your workspace', + description='Pushes any committed changes to the specified recipe to the specified layer and removes it from your workspace. Roughly equivalent to an update-recipe followed by reset, except the update-recipe step will do the "right thing" depending on the recipe and the destination layer specified.', + group='working', order=-100) + parser_finish.add_argument('recipename', help='Recipe to finish') + parser_finish.add_argument('destination', help='Layer/path to put recipe into. Can be the name of a layer configured in your bblayers.conf, the path to the base of a layer, or a partial path inside a layer. %(prog)s will attempt to complete the path based on the layer\'s structure.') + parser_finish.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_finish.add_argument('--initial-rev', help='Override starting revision for patches') + parser_finish.set_defaults(func=finish) diff --git a/scripts/lib/devtool/upgrade.py b/scripts/lib/devtool/upgrade.py index e2be38e7af..05fb9e5ed0 100644 --- a/scripts/lib/devtool/upgrade.py +++ b/scripts/lib/devtool/upgrade.py @@ -27,6 +27,10 @@ import argparse import scriptutils import errno import bb + +devtool_path = os.path.dirname(os.path.realpath(__file__)) + '/../../../meta/lib' +sys.path = sys.path + [devtool_path] + import oe.recipeutils from devtool import standard from devtool import exec_build_env_command, setup_tinfoil, DevtoolError, parse_recipe, use_external_build @@ -68,38 +72,44 @@ def _remove_patch_dirs(recipefolder): shutil.rmtree(os.path.join(root,d)) def _recipe_contains(rd, var): - rf = rd.getVar('FILE', True) + rf = rd.getVar('FILE') varfiles = oe.recipeutils.get_var_files(rf, [var], rd) - for var, fn in varfiles.iteritems(): + for var, fn in varfiles.items(): if fn and fn.startswith(os.path.dirname(rf) + os.sep): return True return False def _rename_recipe_dirs(oldpv, newpv, path): for root, dirs, files in os.walk(path): + # Rename directories with the version in their name for olddir in dirs: if olddir.find(oldpv) != -1: newdir = olddir.replace(oldpv, newpv) if olddir != newdir: - _run('mv %s %s' % (olddir, newdir)) - -def _rename_recipe_file(bpn, oldpv, newpv, path): - oldrecipe = "%s_%s.bb" % (bpn, oldpv) - newrecipe = "%s_%s.bb" % (bpn, newpv) - if os.path.isfile(os.path.join(path, oldrecipe)): + shutil.move(os.path.join(path, olddir), os.path.join(path, newdir)) + # Rename any inc files with the version in their name (unusual, but possible) + for oldfile in files: + if oldfile.endswith('.inc'): + if oldfile.find(oldpv) != -1: + newfile = oldfile.replace(oldpv, newpv) + if oldfile != newfile: + os.rename(os.path.join(path, oldfile), os.path.join(path, newfile)) + +def _rename_recipe_file(oldrecipe, bpn, oldpv, newpv, path): + oldrecipe = os.path.basename(oldrecipe) + if oldrecipe.endswith('_%s.bb' % oldpv): + newrecipe = '%s_%s.bb' % (bpn, newpv) if oldrecipe != newrecipe: - _run('mv %s %s' % (oldrecipe, newrecipe), cwd=path) + shutil.move(os.path.join(path, oldrecipe), os.path.join(path, newrecipe)) else: - recipe = "%s_git.bb" % bpn - if os.path.isfile(os.path.join(path, recipe)): - newrecipe = recipe + newrecipe = oldrecipe return os.path.join(path, newrecipe) -def _rename_recipe_files(bpn, oldpv, newpv, path): +def _rename_recipe_files(oldrecipe, bpn, oldpv, newpv, path): _rename_recipe_dirs(oldpv, newpv, path) - return _rename_recipe_file(bpn, oldpv, newpv, path) + return _rename_recipe_file(oldrecipe, bpn, oldpv, newpv, path) -def _write_append(rc, srctree, same_dir, no_same_dir, rev, workspace, d): +def _write_append(rc, srctree, same_dir, no_same_dir, rev, copied, workspace, d): """Writes an append file""" if not os.path.exists(rc): raise DevtoolError("bbappend not created because %s does not exist" % rc) @@ -111,7 +121,7 @@ def _write_append(rc, srctree, same_dir, no_same_dir, rev, workspace, d): brf = os.path.basename(os.path.splitext(rc)[0]) # rc basename srctree = os.path.abspath(srctree) - pn = d.getVar('PN',True) + pn = d.getVar('PN') af = os.path.join(appendpath, '%s.bbappend' % brf) with open(af, 'w') as f: f.write('FILESEXTRAPATHS_prepend := "${THISDIR}/${PN}:"\n\n') @@ -122,8 +132,12 @@ def _write_append(rc, srctree, same_dir, no_same_dir, rev, workspace, d): b_is_s = use_external_build(same_dir, no_same_dir, d) if b_is_s: f.write('EXTERNALSRC_BUILD_pn-%s = "%s"\n' % (pn, srctree)) + f.write('\n') if rev: - f.write('\n# initial_rev: %s\n' % rev) + f.write('# initial_rev: %s\n' % rev) + if copied: + f.write('# original_path: %s\n' % os.path.dirname(d.getVar('FILE'))) + f.write('# original_files: %s\n' % ' '.join(copied)) return af def _cleanup_on_error(rf, srctree): @@ -144,10 +158,18 @@ def _upgrade_error(e, rf, srctree): raise DevtoolError(e) def _get_uri(rd): - srcuris = rd.getVar('SRC_URI', True).split() + srcuris = rd.getVar('SRC_URI').split() if not len(srcuris): raise DevtoolError('SRC_URI not found on recipe') - srcuri = srcuris[0] # it is assumed, URI is at first position + # Get first non-local entry in SRC_URI - usually by convention it's + # the first entry, but not always! + srcuri = None + for entry in srcuris: + if not entry.startswith('file://'): + srcuri = entry + break + if not srcuri: + raise DevtoolError('Unable to find non-local entry in SRC_URI') srcrev = '${AUTOREV}' if '://' in srcuri: # Fetch a URL @@ -167,7 +189,7 @@ def _extract_new_source(newpv, srctree, no_patch, srcrev, branch, keep_temp, tin crd = rd.createCopy() - pv = crd.getVar('PV', True) + pv = crd.getVar('PV') crd.setVar('PV', newpv) tmpsrctree = None @@ -181,7 +203,7 @@ def _extract_new_source(newpv, srctree, no_patch, srcrev, branch, keep_temp, tin md5 = None sha256 = None else: - __run('git checkout -b devtool-%s' % newpv) + __run('git checkout devtool-base -b devtool-%s' % newpv) tmpdir = tempfile.mkdtemp(prefix='devtool') try: @@ -190,16 +212,28 @@ def _extract_new_source(newpv, srctree, no_patch, srcrev, branch, keep_temp, tin raise DevtoolError(e) tmpsrctree = _get_srctree(tmpdir) + srctree = os.path.abspath(srctree) + + # Delete all sources so we ensure no stray files are left over + for item in os.listdir(srctree): + if item in ['.git', 'oe-local-files']: + continue + itempath = os.path.join(srctree, item) + if os.path.isdir(itempath): + shutil.rmtree(itempath) + else: + os.remove(itempath) - scrtree = os.path.abspath(srctree) - + # Copy in new ones _copy_source_code(tmpsrctree, srctree) (stdout,_) = __run('git ls-files --modified --others --exclude-standard') for f in stdout.splitlines(): __run('git add "%s"' % f) - __run('git commit -q -m "Commit of upstream changes at version %s" --allow-empty' % newpv) + useroptions = [] + oe.patch.GitApplyTree.gitCommandUserOptions(useroptions, d=rd) + __run('git %s commit -q -m "Commit of upstream changes at version %s" --allow-empty' % (' '.join(useroptions), newpv)) __run('git tag -f devtool-base-%s' % newpv) (stdout, _) = __run('git rev-parse HEAD') @@ -212,16 +246,22 @@ def _extract_new_source(newpv, srctree, no_patch, srcrev, branch, keep_temp, tin for patch in patches: logger.warn("%s" % os.path.basename(patch)) else: + __run('git checkout devtool-patched -b %s' % branch) + skiptag = False try: - __run('git checkout devtool-patched -b %s' % branch) __run('git rebase %s' % rev) + except bb.process.ExecutionError as e: + skiptag = True + if 'conflict' in e.stdout: + logger.warn('Command \'%s\' failed:\n%s\n\nYou will need to resolve conflicts in order to complete the upgrade.' % (e.command, e.stdout.rstrip())) + else: + logger.warn('Command \'%s\' failed:\n%s' % (e.command, e.stdout)) + if not skiptag: if uri.startswith('git://'): suffix = 'new' else: suffix = newpv __run('git tag -f devtool-patched-%s' % suffix) - except bb.process.ExecutionError as e: - logger.warn('Command \'%s\' failed:\n%s' % (e.command, e.stdout)) if tmpsrctree: if keep_temp: @@ -233,17 +273,18 @@ def _extract_new_source(newpv, srctree, no_patch, srcrev, branch, keep_temp, tin def _create_new_recipe(newpv, md5, sha256, srcrev, srcbranch, workspace, tinfoil, rd): """Creates the new recipe under workspace""" - crd = rd.createCopy() - bpn = crd.getVar('BPN', True) + bpn = rd.getVar('BPN') path = os.path.join(workspace, 'recipes', bpn) bb.utils.mkdirhier(path) - oe.recipeutils.copy_recipe_files(crd, path) + copied, _ = oe.recipeutils.copy_recipe_files(rd, path) - oldpv = crd.getVar('PV', True) + oldpv = rd.getVar('PV') if not newpv: newpv = oldpv - fullpath = _rename_recipe_files(bpn, oldpv, newpv, path) + origpath = rd.getVar('FILE') + fullpath = _rename_recipe_files(origpath, bpn, oldpv, newpv, path) + logger.debug('Upgraded %s => %s' % (origpath, fullpath)) newvalues = {} if _recipe_contains(rd, 'PV') and newpv != oldpv: @@ -277,15 +318,16 @@ def _create_new_recipe(newpv, md5, sha256, srcrev, srcbranch, workspace, tinfoil if changed: newvalues['SRC_URI'] = ' '.join(new_src_uri) + newvalues['PR'] = None + if md5 and sha256: newvalues['SRC_URI[md5sum]'] = md5 newvalues['SRC_URI[sha256sum]'] = sha256 - if newvalues: - rd = oe.recipeutils.parse_recipe(fullpath, None, tinfoil.config_data) - oe.recipeutils.patch_recipe(rd, fullpath, newvalues) + rd = tinfoil.parse_recipe_file(fullpath, False) + oe.recipeutils.patch_recipe(rd, fullpath, newvalues) - return fullpath + return fullpath, copied def upgrade(args, config, basepath, workspace): """Entry point for the devtool 'upgrade' subcommand""" @@ -297,53 +339,66 @@ def upgrade(args, config, basepath, workspace): if args.srcbranch and not args.srcrev: raise DevtoolError("If you specify --srcbranch/-B then you must use --srcrev/-S to specify the revision" % args.recipename) - reason = oe.recipeutils.validate_pn(args.recipename) - if reason: - raise DevtoolError(reason) - tinfoil = setup_tinfoil(basepath=basepath, tracking=True) - - rd = parse_recipe(config, tinfoil, args.recipename, True) - if not rd: - return 1 - - pn = rd.getVar('PN', True) - if pn != args.recipename: - logger.info('Mapping %s to %s' % (args.recipename, pn)) - if pn in workspace: - raise DevtoolError("recipe %s is already in your workspace" % pn) - - standard._check_compatible_recipe(pn, rd) - if rd.getVar('PV', True) == args.version and rd.getVar('SRCREV', True) == args.srcrev: - raise DevtoolError("Current and upgrade versions are the same version" % version) - - rf = None try: - rev1 = standard._extract_source(args.srctree, False, 'devtool-orig', False, rd) - rev2, md5, sha256 = _extract_new_source(args.version, args.srctree, args.no_patch, - args.srcrev, args.branch, args.keep_temp, - tinfoil, rd) - rf = _create_new_recipe(args.version, md5, sha256, args.srcrev, args.srcbranch, config.workspace_path, tinfoil, rd) - except bb.process.CmdError as e: - _upgrade_error(e, rf, args.srctree) - except DevtoolError as e: - _upgrade_error(e, rf, args.srctree) - standard._add_md5(config, pn, os.path.dirname(rf)) - - af = _write_append(rf, args.srctree, args.same_dir, args.no_same_dir, rev2, - config.workspace_path, rd) - standard._add_md5(config, pn, af) - logger.info('Upgraded source extracted to %s' % args.srctree) + rd = parse_recipe(config, tinfoil, args.recipename, True) + if not rd: + return 1 + + pn = rd.getVar('PN') + if pn != args.recipename: + logger.info('Mapping %s to %s' % (args.recipename, pn)) + if pn in workspace: + raise DevtoolError("recipe %s is already in your workspace" % pn) + + if args.srctree: + srctree = os.path.abspath(args.srctree) + else: + srctree = standard.get_default_srctree(config, pn) + + standard._check_compatible_recipe(pn, rd) + old_srcrev = rd.getVar('SRCREV') + if old_srcrev == 'INVALID': + old_srcrev = None + if old_srcrev and not args.srcrev: + raise DevtoolError("Recipe specifies a SRCREV value; you must specify a new one when upgrading") + if rd.getVar('PV') == args.version and old_srcrev == args.srcrev: + raise DevtoolError("Current and upgrade versions are the same version") + + rf = None + try: + rev1 = standard._extract_source(srctree, False, 'devtool-orig', False, rd, tinfoil) + rev2, md5, sha256 = _extract_new_source(args.version, srctree, args.no_patch, + args.srcrev, args.branch, args.keep_temp, + tinfoil, rd) + rf, copied = _create_new_recipe(args.version, md5, sha256, args.srcrev, args.srcbranch, config.workspace_path, tinfoil, rd) + except bb.process.CmdError as e: + _upgrade_error(e, rf, srctree) + except DevtoolError as e: + _upgrade_error(e, rf, srctree) + standard._add_md5(config, pn, os.path.dirname(rf)) + + af = _write_append(rf, srctree, args.same_dir, args.no_same_dir, rev2, + copied, config.workspace_path, rd) + standard._add_md5(config, pn, af) + logger.info('Upgraded source extracted to %s' % srctree) + logger.info('New recipe is %s' % rf) + finally: + tinfoil.shutdown() return 0 def register_commands(subparsers, context): """Register devtool subcommands from this plugin""" + + defsrctree = standard.get_default_srctree(context.config) + parser_upgrade = subparsers.add_parser('upgrade', help='Upgrade an existing recipe', - description='Upgrades an existing recipe to a new upstream version. Puts the upgraded recipe file into the workspace along with any associated files, and extracts the source tree to a specified location (in case patches need rebasing or adding to as a result of the upgrade).') + description='Upgrades an existing recipe to a new upstream version. Puts the upgraded recipe file into the workspace along with any associated files, and extracts the source tree to a specified location (in case patches need rebasing or adding to as a result of the upgrade).', + group='starting') parser_upgrade.add_argument('recipename', help='Name of recipe to upgrade (just name - no version, path or extension)') - parser_upgrade.add_argument('srctree', help='Path to where to extract the source tree') + parser_upgrade.add_argument('srctree', nargs='?', help='Path to where to extract the source tree. If not specified, a subdirectory of %s will be used.' % defsrctree) parser_upgrade.add_argument('--version', '-V', help='Version to upgrade to (PV)') - parser_upgrade.add_argument('--srcrev', '-S', help='Source revision to upgrade to (if fetching from an SCM such as git)') + parser_upgrade.add_argument('--srcrev', '-S', help='Source revision to upgrade to (required if fetching from an SCM such as git)') parser_upgrade.add_argument('--srcbranch', '-B', help='Branch in source repository containing the revision to use (if fetching from an SCM such as git)') parser_upgrade.add_argument('--branch', '-b', default="devtool", help='Name for new development branch to checkout (default "%(default)s")') parser_upgrade.add_argument('--no-patch', action="store_true", help='Do not apply patches from the recipe to the new source code') diff --git a/scripts/lib/devtool/utilcmds.py b/scripts/lib/devtool/utilcmds.py index a8f5e97833..0437e6417c 100644 --- a/scripts/lib/devtool/utilcmds.py +++ b/scripts/lib/devtool/utilcmds.py @@ -24,6 +24,7 @@ import tempfile import logging import argparse import subprocess +import scriptutils from devtool import exec_build_env_command, setup_tinfoil, check_workspace_recipe, DevtoolError from devtool import parse_recipe @@ -38,7 +39,7 @@ def edit_recipe(args, config, basepath, workspace): rd = parse_recipe(config, tinfoil, args.recipename, True) if not rd: return 1 - recipefile = rd.getVar('FILE', True) + recipefile = rd.getVar('FILE') finally: tinfoil.shutdown() else: @@ -48,17 +49,7 @@ def edit_recipe(args, config, basepath, workspace): raise DevtoolError("Recipe file for %s is not under the workspace" % args.recipename) - editor = os.environ.get('EDITOR', None) - if not editor: - raise DevtoolError("EDITOR environment variable not set") - - import subprocess - try: - subprocess.check_call('%s "%s"' % (editor, recipefile), shell=True) - except subprocess.CalledProcessError as e: - return e.returncode - - return 0 + return scriptutils.run_editor(recipefile) def configure_help(args, config, basepath, workspace): @@ -71,20 +62,20 @@ def configure_help(args, config, basepath, workspace): rd = parse_recipe(config, tinfoil, args.recipename, appends=True, filter_workspace=False) if not rd: return 1 - b = rd.getVar('B', True) - s = rd.getVar('S', True) + b = rd.getVar('B') + s = rd.getVar('S') configurescript = os.path.join(s, 'configure') confdisabled = 'noexec' in rd.getVarFlags('do_configure') or 'do_configure' not in (rd.getVar('__BBTASKS', False) or []) - configureopts = oe.utils.squashspaces(rd.getVar('CONFIGUREOPTS', True) or '') - extra_oeconf = oe.utils.squashspaces(rd.getVar('EXTRA_OECONF', True) or '') - extra_oecmake = oe.utils.squashspaces(rd.getVar('EXTRA_OECMAKE', True) or '') - do_configure = rd.getVar('do_configure', True) or '' + configureopts = oe.utils.squashspaces(rd.getVar('CONFIGUREOPTS') or '') + extra_oeconf = oe.utils.squashspaces(rd.getVar('EXTRA_OECONF') or '') + extra_oecmake = oe.utils.squashspaces(rd.getVar('EXTRA_OECMAKE') or '') + do_configure = rd.getVar('do_configure') or '' do_configure_noexpand = rd.getVar('do_configure', False) or '' packageconfig = rd.getVarFlags('PACKAGECONFIG') or [] autotools = bb.data.inherits_class('autotools', rd) and ('oe_runconf' in do_configure or 'autotools_do_configure' in do_configure) cmake = bb.data.inherits_class('cmake', rd) and ('cmake_do_configure' in do_configure) - cmake_do_configure = rd.getVar('cmake_do_configure', True) - pn = rd.getVar('PN', True) + cmake_do_configure = rd.getVar('cmake_do_configure') + pn = rd.getVar('PN') finally: tinfoil.shutdown() @@ -95,7 +86,7 @@ def configure_help(args, config, basepath, workspace): logger.info('Running do_configure to generate configure script') try: stdout, _ = exec_build_env_command(config.init_path, basepath, - 'bitbake -c configure %s' % msg, args.recipename, + 'bitbake -c configure %s' % args.recipename, stderr=subprocess.STDOUT) except bb.process.ExecutionError: pass @@ -223,7 +214,8 @@ The ./configure %s output for %s follows. def register_commands(subparsers, context): """Register devtool subcommands from this plugin""" parser_edit_recipe = subparsers.add_parser('edit-recipe', help='Edit a recipe file in your workspace', - description='Runs the default editor (as specified by the EDITOR variable) on the specified recipe. Note that the recipe file itself must be in the workspace (i.e. as a result of "devtool add" or "devtool upgrade"); you can override this with the -a/--any-recipe option.') + description='Runs the default editor (as specified by the EDITOR variable) on the specified recipe. Note that the recipe file itself must be in the workspace (i.e. as a result of "devtool add" or "devtool upgrade"); you can override this with the -a/--any-recipe option.', + group='working') parser_edit_recipe.add_argument('recipename', help='Recipe to edit') parser_edit_recipe.add_argument('--any-recipe', '-a', action="store_true", help='Edit any recipe, not just where the recipe file itself is in the workspace') parser_edit_recipe.set_defaults(func=edit_recipe) @@ -232,7 +224,8 @@ def register_commands(subparsers, context): # gets the order wrong - recipename must come before --arg parser_configure_help = subparsers.add_parser('configure-help', help='Get help on configure script options', usage='devtool configure-help [options] recipename [--arg ...]', - description='Displays the help for the configure script for the specified recipe (i.e. runs ./configure --help) prefaced by a header describing the current options being specified. Output is piped through less (or whatever PAGER is set to, if set) for easy browsing.') + description='Displays the help for the configure script for the specified recipe (i.e. runs ./configure --help) prefaced by a header describing the current options being specified. Output is piped through less (or whatever PAGER is set to, if set) for easy browsing.', + group='working') parser_configure_help.add_argument('recipename', help='Recipe to show configure help for') parser_configure_help.add_argument('-p', '--no-pager', help='Disable paged output', action="store_true") parser_configure_help.add_argument('-n', '--no-header', help='Disable explanatory header text', action="store_true") |
