diff options
Diffstat (limited to 'poky/scripts/lib/devtool')
-rw-r--r-- | poky/scripts/lib/devtool/__init__.py | 383 | ||||
-rw-r--r-- | poky/scripts/lib/devtool/build.py | 86 | ||||
-rw-r--r-- | poky/scripts/lib/devtool/build_image.py | 174 | ||||
-rw-r--r-- | poky/scripts/lib/devtool/build_sdk.py | 65 | ||||
-rw-r--r-- | poky/scripts/lib/devtool/deploy.py | 350 | ||||
-rw-r--r-- | poky/scripts/lib/devtool/export.py | 119 | ||||
-rw-r--r-- | poky/scripts/lib/devtool/import.py | 144 | ||||
-rw-r--r-- | poky/scripts/lib/devtool/package.py | 60 | ||||
-rw-r--r-- | poky/scripts/lib/devtool/runqemu.py | 74 | ||||
-rw-r--r-- | poky/scripts/lib/devtool/sdk.py | 336 | ||||
-rw-r--r-- | poky/scripts/lib/devtool/search.py | 118 | ||||
-rw-r--r-- | poky/scripts/lib/devtool/standard.py | 2164 | ||||
-rw-r--r-- | poky/scripts/lib/devtool/upgrade.py | 626 | ||||
-rw-r--r-- | poky/scripts/lib/devtool/utilcmds.py | 252 |
14 files changed, 4951 insertions, 0 deletions
diff --git a/poky/scripts/lib/devtool/__init__.py b/poky/scripts/lib/devtool/__init__.py new file mode 100644 index 000000000..89f098a91 --- /dev/null +++ b/poky/scripts/lib/devtool/__init__.py @@ -0,0 +1,383 @@ +#!/usr/bin/env python3 + +# Development tool - utility functions for plugins +# +# Copyright (C) 2014 Intel Corporation +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +"""Devtool plugins module""" + +import os +import sys +import subprocess +import logging +import re +import codecs + +logger = logging.getLogger('devtool') + +class DevtoolError(Exception): + """Exception for handling devtool errors""" + 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): + """Run a program in bitbake build context""" + import bb + if not 'cwd' in options: + options["cwd"] = builddir + if init_path: + # As the OE init script makes use of BASH_SOURCE to determine OEROOT, + # and can't determine it when running under dash, we need to set + # the executable to bash to correctly set things up + if not 'executable' in options: + options['executable'] = 'bash' + logger.debug('Executing command: "%s" using init path %s' % (cmd, init_path)) + init_prefix = '. %s %s > /dev/null && ' % (init_path, builddir) + else: + logger.debug('Executing command "%s"' % cmd) + init_prefix = '' + if watch: + if sys.stdout.isatty(): + # Fool bitbake into thinking it's outputting to a terminal (because it is, indirectly) + cmd = 'script -e -q -c "%s" /dev/null' % cmd + return exec_watch('%s%s' % (init_prefix, cmd), **options) + else: + return bb.process.run('%s%s' % (init_prefix, cmd), **options) + +def exec_watch(cmd, **options): + """Run program with stdout shown on sys.stdout""" + import bb + 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 = reader.read(1, 1) + if out: + sys.stdout.write(out) + sys.stdout.flush() + buf += out + elif out == '' and process.poll() != None: + break + + if process.returncode != 0: + raise bb.process.ExecutionError(cmd, process.returncode, buf, None) + + return buf, None + +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') + 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') + for varvalue in fakerootenv.split(): + if '=' in varvalue: + splitval = varvalue.split('=', 1) + newenv[splitval[0]] = splitval[1] + return subprocess.call("%s %s" % (fakerootcmd, cmd), env=newenv, **kwargs) + +def setup_tinfoil(config_only=False, basepath=None, tracking=False): + """Initialize tinfoil api from bitbake""" + import scriptpath + orig_cwd = os.path.abspath(os.curdir) + try: + if basepath: + os.chdir(basepath) + bitbakepath = scriptpath.add_bitbake_lib_path() + if not bitbakepath: + logger.error("Unable to find bitbake by searching parent directory of this script or PATH") + sys.exit(1) + + import bb.tinfoil + tinfoil = bb.tinfoil.Tinfoil(tracking=tracking) + try: + tinfoil.logger.setLevel(logger.getEffectiveLevel()) + tinfoil.prepare(config_only) + 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 parse_recipe(config, tinfoil, pn, appends, filter_workspace=True): + """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.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 + 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): + """ + Check that a recipe is in the workspace and (optionally) that source + is present. + """ + + workspacepn = pn + + for recipe, value in workspace.items(): + if recipe == pn: + break + if bbclassextend: + recipefile = value['recipefile'] + if recipefile: + targets = get_bbclassextend_targets(recipefile, recipe) + if pn in targets: + workspacepn = recipe + break + else: + raise DevtoolError("No recipe named '%s' in your workspace" % pn) + + if checksrc: + srctree = workspace[workspacepn]['srctree'] + if not os.path.exists(srctree): + raise DevtoolError("Source tree %s for recipe %s does not exist" % (srctree, workspacepn)) + if not os.listdir(srctree): + raise DevtoolError("Source tree %s for recipe %s is empty" % (srctree, workspacepn)) + + return workspacepn + +def use_external_build(same_dir, no_same_dir, d): + """ + Determine if we should use B!=S (separate build and source directories) or not + """ + b_is_s = True + if no_same_dir: + logger.info('Using separate build directory since --no-same-dir specified') + b_is_s = False + elif same_dir: + 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 os.path.abspath(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', 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'] + 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') + commitmsg = "Initial empty commit with no upstream sources" + elif version: + commitmsg = "Initial commit from upstream at version %s" % version + else: + commitmsg = "Initial commit from upstream" + commit_cmd += ['-m', commitmsg] + bb.process.run(commit_cmd, cwd=repodir) + + # Ensure singletask.lock (as used by externalsrc.bbclass) is ignored by git + excludes = [] + excludefile = os.path.join(repodir, '.git', 'info', 'exclude') + try: + with open(excludefile, 'r') as f: + excludes = f.readlines() + except FileNotFoundError: + pass + if 'singletask.lock\n' not in excludes: + excludes.append('singletask.lock\n') + with open(excludefile, 'w') as f: + for line in excludes: + f.write(line) + + bb.process.run('git checkout -b %s' % devbranch, cwd=repodir) + bb.process.run('git tag -f %s' % basetag, cwd=repodir) + +def recipe_to_append(recipefile, config, wildcard=False): + """ + Convert a recipe file to a bbappend file path within the workspace. + NOTE: if the bbappend already exists, you should be using + workspace[args.recipename]['bbappend'] instead of calling this + function. + """ + appendname = os.path.splitext(os.path.basename(recipefile))[0] + if wildcard: + appendname = re.sub(r'_.*', '_%', appendname) + appendpath = os.path.join(config.workspace_path, 'appends') + appendfile = os.path.join(appendpath, appendname + '.bbappend') + return appendfile + +def get_bbclassextend_targets(recipefile, pn): + """ + Cheap function to get BBCLASSEXTEND and then convert that to the + list of targets that would result. + """ + import bb.utils + + values = {} + def get_bbclassextend_varfunc(varname, origvalue, op, newlines): + values[varname] = origvalue + return origvalue, None, 0, True + with open(recipefile, 'r') as f: + bb.utils.edit_metadata(f, ['BBCLASSEXTEND'], get_bbclassextend_varfunc) + + targets = [] + bbclassextend = values.get('BBCLASSEXTEND', '').split() + if bbclassextend: + for variant in bbclassextend: + if variant == 'nativesdk': + targets.append('%s-%s' % (variant, pn)) + elif variant in ['native', 'cross', 'crosssdk']: + targets.append('%s-%s' % (pn, variant)) + return targets + +def replace_from_file(path, old, new): + """Replace strings on a file""" + + def read_file(path): + data = None + with open(path) as f: + data = f.read() + return data + + def write_file(path, data): + if data is None: + return + wdata = data.rstrip() + "\n" + with open(path, "w") as f: + f.write(wdata) + + # In case old is None, return immediately + if old is None: + return + try: + rdata = read_file(path) + except IOError as e: + # if file does not exit, just quit, otherwise raise an exception + if e.errno == errno.ENOENT: + return + else: + raise + + old_contents = rdata.splitlines() + new_contents = [] + for old_content in old_contents: + try: + new_contents.append(old_content.replace(old, new)) + except ValueError: + pass + write_file(path, "\n".join(new_contents)) + + +def update_unlockedsigs(basepath, workspace, fixed_setup, extra=None): + """ This function will make unlocked-sigs.inc match the recipes in the + workspace plus any extras we want unlocked. """ + + if not fixed_setup: + # Only need to write this out within the eSDK + return + + if not extra: + extra = [] + + confdir = os.path.join(basepath, 'conf') + unlockedsigs = os.path.join(confdir, 'unlocked-sigs.inc') + + # Get current unlocked list if any + values = {} + def get_unlockedsigs_varfunc(varname, origvalue, op, newlines): + values[varname] = origvalue + return origvalue, None, 0, True + if os.path.exists(unlockedsigs): + with open(unlockedsigs, 'r') as f: + bb.utils.edit_metadata(f, ['SIGGEN_UNLOCKED_RECIPES'], get_unlockedsigs_varfunc) + unlocked = sorted(values.get('SIGGEN_UNLOCKED_RECIPES', [])) + + # If the new list is different to the current list, write it out + newunlocked = sorted(list(workspace.keys()) + extra) + if unlocked != newunlocked: + bb.utils.mkdirhier(confdir) + with open(unlockedsigs, 'w') as f: + f.write("# DO NOT MODIFY! YOUR CHANGES WILL BE LOST.\n" + + "# This layer was created by the OpenEmbedded devtool" + + " utility in order to\n" + + "# contain recipes that are unlocked.\n") + + f.write('SIGGEN_UNLOCKED_RECIPES += "\\\n') + for pn in newunlocked: + f.write(' ' + pn) + f.write('"') + +def check_prerelease_version(ver, operation): + if 'pre' in ver or 'rc' in ver: + logger.warning('Version "%s" looks like a pre-release version. ' + 'If that is the case, in order to ensure that the ' + 'version doesn\'t appear to go backwards when you ' + 'later upgrade to the final release version, it is ' + 'recommmended that instead you use ' + '<current version>+<pre-release version> e.g. if ' + 'upgrading from 1.9 to 2.0-rc2 use "1.9+2.0-rc2". ' + 'If you prefer not to reset and re-try, you can change ' + 'the version after %s succeeds using "devtool rename" ' + 'with -V/--version.' % (ver, operation)) + +def check_git_repo_dirty(repodir): + """Check if a git repository is clean or not""" + stdout, _ = bb.process.run('git status --porcelain', cwd=repodir) + return stdout + +def check_git_repo_op(srctree, ignoredirs=None): + """Check if a git repository is in the middle of a rebase""" + stdout, _ = bb.process.run('git rev-parse --show-toplevel', cwd=srctree) + topleveldir = stdout.strip() + if ignoredirs and topleveldir in ignoredirs: + return + gitdir = os.path.join(topleveldir, '.git') + if os.path.exists(os.path.join(gitdir, 'rebase-merge')): + raise DevtoolError("Source tree %s appears to be in the middle of a rebase - please resolve this first" % srctree) + if os.path.exists(os.path.join(gitdir, 'rebase-apply')): + raise DevtoolError("Source tree %s appears to be in the middle of 'git am' or 'git apply' - please resolve this first" % srctree) diff --git a/poky/scripts/lib/devtool/build.py b/poky/scripts/lib/devtool/build.py new file mode 100644 index 000000000..252379e9b --- /dev/null +++ b/poky/scripts/lib/devtool/build.py @@ -0,0 +1,86 @@ +# Development tool - build command plugin +# +# Copyright (C) 2014-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 build plugin""" + +import os +import bb +import logging +import argparse +import tempfile +from devtool import exec_build_env_command, check_workspace_recipe, DevtoolError + +logger = logging.getLogger('devtool') + + +def _set_file_values(fn, values): + remaining = list(values.keys()) + + def varfunc(varname, origvalue, op, newlines): + newvalue = values.get(varname, origvalue) + remaining.remove(varname) + return (newvalue, '=', 0, True) + + with open(fn, 'r') as f: + (updated, newlines) = bb.utils.edit_metadata(f, values, varfunc) + + for item in remaining: + updated = True + newlines.append('%s = "%s"' % (item, values[item])) + + if updated: + with open(fn, 'w') as f: + f.writelines(newlines) + return updated + +def _get_build_tasks(config): + tasks = config.get('Build', 'build_task', 'populate_sysroot,packagedata').split(',') + return ['do_%s' % task.strip() for task in tasks] + +def build(args, config, basepath, workspace): + """Entry point for the devtool 'build' subcommand""" + workspacepn = check_workspace_recipe(workspace, args.recipename, bbclassextend=True) + + build_tasks = _get_build_tasks(config) + + bbappend = workspace[workspacepn]['bbappend'] + if args.disable_parallel_make: + logger.info("Disabling 'make' parallelism") + _set_file_values(bbappend, {'PARALLEL_MAKE': ''}) + try: + bbargs = [] + for task in build_tasks: + if args.recipename.endswith('-native') and 'package' in task: + continue + bbargs.append('%s:%s' % (args.recipename, task)) + exec_build_env_command(config.init_path, basepath, 'bitbake %s' % ' '.join(bbargs), watch=True) + 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 + finally: + if args.disable_parallel_make: + _set_file_values(bbappend, {'PARALLEL_MAKE': None}) + + return 0 + +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)), + 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/poky/scripts/lib/devtool/build_image.py b/poky/scripts/lib/devtool/build_image.py new file mode 100644 index 000000000..e5810389b --- /dev/null +++ b/poky/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/poky/scripts/lib/devtool/build_sdk.py b/poky/scripts/lib/devtool/build_sdk.py new file mode 100644 index 000000000..b89d65b0c --- /dev/null +++ b/poky/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/poky/scripts/lib/devtool/deploy.py b/poky/scripts/lib/devtool/deploy.py new file mode 100644 index 000000000..52e261d56 --- /dev/null +++ b/poky/scripts/lib/devtool/deploy.py @@ -0,0 +1,350 @@ +# Development tool - deploy/undeploy command plugin +# +# 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 +# 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 deploy subcommands""" + +import logging +import os +import shutil +import subprocess +import tempfile + +import bb.utils +import argparse_oe +import oe.types + +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 -f $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') + # find from busybox might not have -exec, so we don't use that + lines.append(' find . -type f | while read file') + lines.append(' do') + lines.append(' mv $file /$file') + lines.append(' done') + 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 math + import oe.recipeutils + import oe.package + + check_workspace_recipe(workspace, args.recipename, checksrc=False) + + try: + host, destdir = args.target.split(':') + except ValueError: + destdir = '/' + else: + args.target = host + if not destdir.endswith('/'): + destdir += '/' + + tinfoil = setup_tinfoil(basepath=basepath) + try: + 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) + + if args.strip and not args.dry_run: + # Fakeroot copy to new destination + srcdir = recipe_outdir + recipe_outdir = os.path.join(rd.getVar('WORKDIR'), 'deploy-target-stripped') + if os.path.isdir(recipe_outdir): + bb.utils.remove(recipe_outdir, True) + exec_fakeroot(rd, "cp -af %s %s" % (os.path.join(srcdir, '.'), recipe_outdir), shell=True) + os.environ['PATH'] = ':'.join([os.environ['PATH'], rd.getVar('PATH') or '']) + oe.package.strip_execs(args.recipename, recipe_outdir, rd.getVar('STRIP'), rd.getVar('libdir'), + rd.getVar('base_libdir')) + + filelist = [] + ftotalsize = 0 + for root, _, files in os.walk(recipe_outdir): + for fn in files: + # 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 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 not args.show_status: + extraoptions += ' -q' + + scp_port = '' + ssh_port = '' + if args.port: + scp_port = "-P %s" % args.port + ssh_port = "-p %s" % args.port + + # 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) + + # 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') + + 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""" + 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: + extraoptions += '-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no' + if not args.show_status: + extraoptions += ' -q' + + scp_port = '' + ssh_port = '' + if args.port: + 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') + + 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', + 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', help='Specify port to use for connection to the target') + + strip_opts = parser_deploy.add_mutually_exclusive_group(required=False) + strip_opts.add_argument('-S', '--strip', + help='Strip executables prior to deploying (default: %(default)s). ' + 'The default value of this option can be controlled by setting the strip option in the [Deploy] section to True or False.', + default=oe.types.boolean(context.config.get('Deploy', 'strip', default='0')), + action='store_true') + strip_opts.add_argument('--no-strip', help='Do not strip executables prior to deploy', dest='strip', action='store_false') + + parser_deploy.set_defaults(func=deploy) + + 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', help='Specify port to use for connection to the target') + parser_undeploy.set_defaults(func=undeploy) diff --git a/poky/scripts/lib/devtool/export.py b/poky/scripts/lib/devtool/export.py new file mode 100644 index 000000000..13ee258e7 --- /dev/null +++ b/poky/scripts/lib/devtool/export.py @@ -0,0 +1,119 @@ +# Development tool - export command plugin +# +# Copyright (C) 2014-2017 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 export plugin""" + +import os +import argparse +import tarfile +import logging +import datetime +import json + +logger = logging.getLogger('devtool') + +# output files +default_arcname_prefix = "workspace-export" +metadata = '.export_metadata' + +def export(args, config, basepath, workspace): + """Entry point for the devtool 'export' subcommand""" + + def add_metadata(tar): + """Archive the workspace object""" + # finally store the workspace metadata + with open(metadata, 'w') as fd: + fd.write(json.dumps((config.workspace_path, workspace))) + tar.add(metadata) + os.unlink(metadata) + + def add_recipe(tar, recipe, data): + """Archive recipe with proper arcname""" + # Create a map of name/arcnames + arcnames = [] + for key, name in data.items(): + if name: + if key == 'srctree': + # all sources, no matter where are located, goes into the sources directory + arcname = 'sources/%s' % recipe + else: + arcname = name.replace(config.workspace_path, '') + arcnames.append((name, arcname)) + + for name, arcname in arcnames: + tar.add(name, arcname=arcname) + + + # Make sure workspace is non-empty and possible listed include/excluded recipes are in workspace + if not workspace: + logger.info('Workspace contains no recipes, nothing to export') + return 0 + else: + for param, recipes in {'include':args.include,'exclude':args.exclude}.items(): + for recipe in recipes: + if recipe not in workspace: + logger.error('Recipe (%s) on %s argument not in the current workspace' % (recipe, param)) + return 1 + + name = args.file + + default_name = "%s-%s.tar.gz" % (default_arcname_prefix, datetime.datetime.now().strftime('%Y%m%d%H%M%S')) + if not name: + name = default_name + else: + # if name is a directory, append the default name + if os.path.isdir(name): + name = os.path.join(name, default_name) + + if os.path.exists(name) and not args.overwrite: + logger.error('Tar archive %s exists. Use --overwrite/-o to overwrite it') + return 1 + + # if all workspace is excluded, quit + if not len(set(workspace.keys()).difference(set(args.exclude))): + logger.warn('All recipes in workspace excluded, nothing to export') + return 0 + + exported = [] + with tarfile.open(name, 'w:gz') as tar: + if args.include: + for recipe in args.include: + add_recipe(tar, recipe, workspace[recipe]) + exported.append(recipe) + else: + for recipe, data in workspace.items(): + if recipe not in args.exclude: + add_recipe(tar, recipe, data) + exported.append(recipe) + + add_metadata(tar) + + logger.info('Tar archive created at %s with the following recipes: %s' % (name, ', '.join(exported))) + return 0 + +def register_commands(subparsers, context): + """Register devtool export subcommands""" + parser = subparsers.add_parser('export', + help='Export workspace into a tar archive', + description='Export one or more recipes from current workspace into a tar archive', + group='advanced') + + parser.add_argument('--file', '-f', help='Output archive file name') + parser.add_argument('--overwrite', '-o', action="store_true", help='Overwrite previous export tar archive') + group = parser.add_mutually_exclusive_group() + group.add_argument('--include', '-i', nargs='+', default=[], help='Include recipes into the tar archive') + group.add_argument('--exclude', '-e', nargs='+', default=[], help='Exclude recipes into the tar archive') + parser.set_defaults(func=export) diff --git a/poky/scripts/lib/devtool/import.py b/poky/scripts/lib/devtool/import.py new file mode 100644 index 000000000..c13a180d1 --- /dev/null +++ b/poky/scripts/lib/devtool/import.py @@ -0,0 +1,144 @@ +# Development tool - import command plugin +# +# Copyright (C) 2014-2017 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 import plugin""" + +import os +import tarfile +import logging +import collections +import json +import fnmatch + +from devtool import standard, setup_tinfoil, replace_from_file, DevtoolError +from devtool import export + +logger = logging.getLogger('devtool') + +def devimport(args, config, basepath, workspace): + """Entry point for the devtool 'import' subcommand""" + + def get_pn(name): + """ Returns the filename of a workspace recipe/append""" + metadata = name.split('/')[-1] + fn, _ = os.path.splitext(metadata) + return fn + + if not os.path.exists(args.file): + raise DevtoolError('Tar archive %s does not exist. Export your workspace using "devtool export"' % args.file) + + with tarfile.open(args.file) as tar: + # Get exported metadata + export_workspace_path = export_workspace = None + try: + metadata = tar.getmember(export.metadata) + except KeyError as ke: + raise DevtoolError('The export metadata file created by "devtool export" was not found. "devtool import" can only be used to import tar archives created by "devtool export".') + + tar.extract(metadata) + with open(metadata.name) as fdm: + export_workspace_path, export_workspace = json.load(fdm) + os.unlink(metadata.name) + + members = tar.getmembers() + + # Get appends and recipes from the exported archive, these + # will be needed to find out those appends without corresponding + # recipe pair + append_fns, recipe_fns = set(), set() + for member in members: + if member.name.startswith('appends'): + append_fns.add(get_pn(member.name)) + elif member.name.startswith('recipes'): + recipe_fns.add(get_pn(member.name)) + + # Setup tinfoil, get required data and shutdown + tinfoil = setup_tinfoil(config_only=False, basepath=basepath) + try: + current_fns = [os.path.basename(recipe[0]) for recipe in tinfoil.cooker.recipecaches[''].pkg_fn.items()] + finally: + tinfoil.shutdown() + + # Find those appends that do not have recipes in current metadata + non_importables = [] + for fn in append_fns - recipe_fns: + # Check on current metadata (covering those layers indicated in bblayers.conf) + for current_fn in current_fns: + if fnmatch.fnmatch(current_fn, '*' + fn.replace('%', '') + '*'): + break + else: + non_importables.append(fn) + logger.warn('No recipe to append %s.bbapppend, skipping' % fn) + + # Extract + imported = [] + for member in members: + if member.name == export.metadata: + continue + + for nonimp in non_importables: + pn = nonimp.split('_')[0] + # do not extract data from non-importable recipes or metadata + if member.name.startswith('appends/%s' % nonimp) or \ + member.name.startswith('recipes/%s' % nonimp) or \ + member.name.startswith('sources/%s' % pn): + break + else: + path = os.path.join(config.workspace_path, member.name) + if os.path.exists(path): + # by default, no file overwrite is done unless -o is given by the user + if args.overwrite: + try: + tar.extract(member, path=config.workspace_path) + except PermissionError as pe: + logger.warn(pe) + else: + logger.warn('File already present. Use --overwrite/-o to overwrite it: %s' % member.name) + continue + else: + tar.extract(member, path=config.workspace_path) + + # Update EXTERNALSRC and the devtool md5 file + if member.name.startswith('appends'): + if export_workspace_path: + # appends created by 'devtool modify' just need to update the workspace + replace_from_file(path, export_workspace_path, config.workspace_path) + + # appends created by 'devtool add' need replacement of exported source tree + pn = get_pn(member.name).split('_')[0] + exported_srctree = export_workspace[pn]['srctree'] + if exported_srctree: + replace_from_file(path, exported_srctree, os.path.join(config.workspace_path, 'sources', pn)) + + standard._add_md5(config, pn, path) + imported.append(pn) + + if imported: + logger.info('Imported recipes into workspace %s: %s' % (config.workspace_path, ', '.join(imported))) + else: + logger.warn('No recipes imported into the workspace') + + return 0 + +def register_commands(subparsers, context): + """Register devtool import subcommands""" + parser = subparsers.add_parser('import', + help='Import exported tar archive into workspace', + description='Import tar archive previously created by "devtool export" into workspace', + group='advanced') + parser.add_argument('file', metavar='FILE', help='Name of the tar archive to import') + parser.add_argument('--overwrite', '-o', action="store_true", help='Overwrite files when extracting') + parser.set_defaults(func=devimport) diff --git a/poky/scripts/lib/devtool/package.py b/poky/scripts/lib/devtool/package.py new file mode 100644 index 000000000..af9e8f15f --- /dev/null +++ b/poky/scripts/lib/devtool/package.py @@ -0,0 +1,60 @@ +# Development tool - package command plugin +# +# Copyright (C) 2014-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 package subcommands""" + +import os +import subprocess +import logging +from bb.process import ExecutionError +from devtool import exec_build_env_command, setup_tinfoil, check_workspace_recipe, DevtoolError + +logger = logging.getLogger('devtool') + +def package(args, config, basepath, workspace): + """Entry point for the devtool 'package' subcommand""" + check_workspace_recipe(workspace, args.recipename) + + tinfoil = setup_tinfoil(basepath=basepath, config_only=True) + try: + image_pkgtype = config.get('Package', 'image_pkgtype', '') + if not image_pkgtype: + image_pkgtype = tinfoil.config_data.getVar('IMAGE_PKGTYPE') + + deploy_dir_pkg = tinfoil.config_data.getVar('DEPLOY_DIR_%s' % image_pkgtype.upper()) + finally: + tinfoil.shutdown() + + package_task = config.get('Package', 'package_task', 'package_write_%s' % image_pkgtype) + try: + exec_build_env_command(config.init_path, basepath, 'bitbake -c %s %s' % (package_task, args.recipename), watch=True) + 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 + + logger.info('Your packages are in %s' % deploy_dir_pkg) + + return 0 + +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', + group='testbuild', order=-5) + parser_package.add_argument('recipename', help='Recipe to package') + parser_package.set_defaults(func=package) diff --git a/poky/scripts/lib/devtool/runqemu.py b/poky/scripts/lib/devtool/runqemu.py new file mode 100644 index 000000000..e26cf28c2 --- /dev/null +++ b/poky/scripts/lib/devtool/runqemu.py @@ -0,0 +1,74 @@ +# Development tool - runqemu command 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 runqemu plugin""" + +import os +import bb +import logging +import argparse +import glob +from devtool import exec_build_env_command, setup_tinfoil, DevtoolError + +logger = logging.getLogger('devtool') + +def runqemu(args, config, basepath, workspace): + """Entry point for the devtool 'runqemu' subcommand""" + + tinfoil = setup_tinfoil(config_only=True, basepath=basepath) + 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') + + imagename = args.imagename + if not imagename: + sdk_targets = config.get('SDK', 'sdk_targets', '').split() + if sdk_targets: + imagename = sdk_targets[0] + if not imagename: + raise DevtoolError('Unable to determine image name to run, please specify one') + + try: + # 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 + + return 0 + +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', + 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) + parser_runqemu.set_defaults(func=runqemu) diff --git a/poky/scripts/lib/devtool/sdk.py b/poky/scripts/lib/devtool/sdk.py new file mode 100644 index 000000000..f46577c2a --- /dev/null +++ b/poky/scripts/lib/devtool/sdk.py @@ -0,0 +1,336 @@ +# 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 +import logging +import glob +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') + +def parse_locked_sigs(sigfile_path): + """Return <pn:task>:<hash> dictionary""" + sig_dict = {} + with open(sigfile_path) as f: + lines = f.readlines() + for line in lines: + if ':' in line: + taskkey, _, hashval = line.rpartition(':') + sig_dict[taskkey.strip()] = hashval.split()[0] + return sig_dict + +def generate_update_dict(sigfile_new, sigfile_old): + """Return a dict containing <pn:task>:<hash> which indicates what need to be updated""" + update_dict = {} + sigdict_new = parse_locked_sigs(sigfile_new) + sigdict_old = parse_locked_sigs(sigfile_old) + for k in sigdict_new: + if k not in sigdict_old: + update_dict[k] = sigdict_new[k] + continue + if sigdict_new[k] != sigdict_old[k]: + update_dict[k] = sigdict_new[k] + continue + return update_dict + +def get_sstate_objects(update_dict, sstate_dir): + """Return a list containing sstate objects which are to be installed""" + sstate_objects = [] + for k in update_dict: + files = set() + hashval = update_dict[k] + p = sstate_dir + '/' + hashval[:2] + '/*' + hashval + '*.tgz' + files |= set(glob.glob(p)) + p = sstate_dir + '/*/' + hashval[:2] + '/*' + hashval + '*.tgz' + files |= set(glob.glob(p)) + files = list(files) + if len(files) == 1: + sstate_objects.extend(files) + elif len(files) > 1: + logger.error("More than one matching sstate object found for %s" % hashval) + + return sstate_objects + +def mkdir(d): + try: + os.makedirs(d) + except OSError as e: + if e.errno != errno.EEXIST: + raise e + +def install_sstate_objects(sstate_objects, src_sdk, dest_sdk): + """Install sstate objects into destination SDK""" + sstate_dir = os.path.join(dest_sdk, 'sstate-cache') + if not os.path.exists(sstate_dir): + logger.error("Missing sstate-cache directory in %s, it might not be an extensible SDK." % dest_sdk) + raise + for sb in sstate_objects: + dst = sb.replace(src_sdk, dest_sdk) + destdir = os.path.dirname(dst) + mkdir(destdir) + logger.debug("Copying %s to %s" % (sb, dst)) + shutil.copy(sb, dst) + +def check_manifest(fn, basepath): + import bb.utils + changedfiles = [] + with open(fn, 'r') as f: + for line in f: + splitline = line.split() + if len(splitline) > 1: + chksum = splitline[0] + fpath = splitline[1] + curr_chksum = bb.utils.sha256_file(os.path.join(basepath, fpath)) + if chksum != curr_chksum: + logger.debug('File %s changed: old csum = %s, new = %s' % (os.path.join(basepath, fpath), curr_chksum, chksum)) + changedfiles.append(fpath) + return changedfiles + +def sdk_update(args, config, basepath, workspace): + """Entry point for devtool sdk-update command""" + updateserver = args.updateserver + if not updateserver: + updateserver = config.get('SDK', 'updateserver', '') + logger.debug("updateserver: %s" % updateserver) + + # Make sure we are using sdk-update from within SDK + logger.debug("basepath = %s" % basepath) + old_locked_sig_file_path = os.path.join(basepath, 'conf/locked-sigs.inc') + if not os.path.exists(old_locked_sig_file_path): + logger.error("Not using devtool's sdk-update command from within an extensible SDK. Please specify correct basepath via --basepath option") + return -1 + else: + logger.debug("Found conf/locked-sigs.inc in %s" % basepath) + + 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') + + # Grab variable values + tinfoil = setup_tinfoil(config_only=True, basepath=basepath) + try: + 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() + + 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 ...") + #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 @{u}", shell=True, cwd=layers_dir) + else: + 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: + 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 + + 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) + + # 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 + sdk_update_targets = [] + tasks = ['do_populate_sysroot', 'do_packagedata'] + for root, _, files in os.walk(stamps_dir): + for fn in files: + if not '.sigdata.' in fn: + for task in tasks: + if '.%s.' % task in fn or '.%s_setscene.' % task in fn: + sdk_update_targets.append('%s:%s' % (os.path.basename(root), task)) + # Run bitbake command for the whole SDK + logger.info("Preparing build system... (This may take some time.)") + try: + exec_build_env_command(config.init_path, basepath, 'bitbake --setscene-only %s' % ' '.join(sdk_update_targets), stderr=subprocess.STDOUT) + output, _ = exec_build_env_command(config.init_path, basepath, 'bitbake -n %s' % ' '.join(sdk_update_targets), stderr=subprocess.STDOUT) + runlines = [] + for line in output.splitlines(): + if 'Running task ' in line: + runlines.append(line) + if runlines: + logger.error('Unexecuted tasks found in preparation log:\n %s' % '\n '.join(runlines)) + return -1 + except bb.process.ExecutionError as e: + logger.error('Preparation failed:\n%s' % e.stdout) + return -1 + return 0 + +def sdk_install(args, config, basepath, workspace): + """Entry point for the devtool sdk-install command""" + + import oe.recipeutils + import bb.process + + for recipe in args.recipename: + if recipe in workspace: + raise DevtoolError('recipe %s is a recipe in your workspace' % recipe) + + tasks = ['do_populate_sysroot', 'do_packagedata'] + stampprefixes = {} + def checkstamp(recipe): + stampprefix = stampprefixes[recipe] + stamps = glob.glob(stampprefix + '*') + for stamp in stamps: + if '.sigdata.' not in stamp and stamp.startswith((stampprefix + '.', stampprefix + '_setscene.')): + return True + else: + return False + + install_recipes = [] + tinfoil = setup_tinfoil(config_only=False, basepath=basepath) + try: + for recipe in args.recipename: + rd = parse_recipe(config, tinfoil, recipe, True) + if not rd: + return 1 + stampprefixes[recipe] = '%s.%s' % (rd.getVar('STAMP'), tasks[0]) + if checkstamp(recipe): + logger.info('%s is already installed' % recipe) + else: + install_recipes.append(recipe) + finally: + tinfoil.shutdown() + + if install_recipes: + logger.info('Installing %s...' % ', '.join(install_recipes)) + install_tasks = [] + for recipe in install_recipes: + for task in tasks: + 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 %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 + for recipe in install_recipes: + if checkstamp(recipe): + logger.info('Successfully installed %s' % recipe) + else: + raise DevtoolError('Failed to install %s - unavailable' % recipe) + failed = True + 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', + 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.)', + 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/poky/scripts/lib/devtool/search.py b/poky/scripts/lib/devtool/search.py new file mode 100644 index 000000000..b4f209b7e --- /dev/null +++ b/poky/scripts/lib/devtool/search.py @@ -0,0 +1,118 @@ +# Development tool - search command 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 search plugin""" + +import os +import bb +import logging +import argparse +import re +from devtool import setup_tinfoil, parse_recipe, DevtoolError + +logger = logging.getLogger('devtool') + +def search(args, config, basepath, workspace): + """Entry point for the devtool 'search' subcommand""" + + tinfoil = setup_tinfoil(config_only=False, basepath=basepath) + try: + pkgdata_dir = tinfoil.config_data.getVar('PKGDATA_DIR') + defsummary = tinfoil.config_data.getVar('SUMMARY', False) or '' + + keyword_rc = re.compile(args.keyword) + + def print_match(pn): + rd = parse_recipe(config, tinfoil, pn, True) + if not rd: + return + summary = rd.getVar('SUMMARY') + if summary == rd.expand(defsummary): + summary = '' + print("%s %s" % (pn.ljust(20), summary)) + + + matches = [] + if os.path.exists(pkgdata_dir): + for fn in os.listdir(pkgdata_dir): + pfn = os.path.join(pkgdata_dir, fn) + if not os.path.isfile(pfn): + continue + + packages = [] + match = False + if keyword_rc.search(fn): + match = True + + if not match: + with open(pfn, 'r') as f: + for line in f: + if line.startswith('PACKAGES:'): + packages = line.split(':', 1)[1].strip().split() + + for pkg in packages: + if keyword_rc.search(pkg): + match = True + break + if os.path.exists(os.path.join(pkgdata_dir, 'runtime', pkg + '.packaged')): + with open(os.path.join(pkgdata_dir, 'runtime', pkg), 'r') as f: + for line in f: + if ': ' in line: + splitline = line.split(':', 1) + key = splitline[0] + value = splitline[1].strip() + if key in ['PKG_%s' % pkg, 'DESCRIPTION', 'FILES_INFO'] or key.startswith('FILERPROVIDES_'): + if keyword_rc.search(value): + match = True + break + if match: + print_match(fn) + matches.append(fn) + else: + logger.warning('Package data is not available, results may be limited') + + for recipe in tinfoil.all_recipes(): + if args.fixed_setup and 'nativesdk' in recipe.inherits(): + continue + + match = False + if keyword_rc.search(recipe.pn): + match = True + else: + for prov in recipe.provides: + if keyword_rc.search(prov): + match = True + break + if not match: + for rprov in recipe.rprovides: + if keyword_rc.search(rprov): + match = True + break + if match and not recipe.pn in matches: + print_match(recipe.pn) + finally: + tinfoil.shutdown() + + return 0 + +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 recipes. Matches on recipe name, package name, description and installed files, and prints the recipe name and summary on match.', + group='info') + parser_search.add_argument('keyword', help='Keyword to search for (regular expression syntax allowed, use quotes to avoid shell expansion)') + parser_search.set_defaults(func=search, no_workspace=True, fixed_setup=context.fixed_setup) diff --git a/poky/scripts/lib/devtool/standard.py b/poky/scripts/lib/devtool/standard.py new file mode 100644 index 000000000..a1e8e1d32 --- /dev/null +++ b/poky/scripts/lib/devtool/standard.py @@ -0,0 +1,2164 @@ +# Development tool - standard commands plugin +# +# Copyright (C) 2014-2017 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 standard plugins""" + +import os +import sys +import re +import shutil +import subprocess +import tempfile +import logging +import argparse +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, update_unlockedsigs, check_prerelease_version, check_git_repo_dirty, check_git_repo_op, DevtoolError +from devtool import parse_recipe + +logger = logging.getLogger('devtool') + +override_branch_prefix = 'devtool-override-' + + +def add(args, config, basepath, workspace): + """Entry point for the devtool 'add' subcommand""" + import bb + import oe.recipeutils + + if not args.recipename and not args.srctree and not args.fetch and not args.fetchuri: + raise argparse_oe.ArgumentUsageError('At least one of recipename, srctree, fetchuri or -f/--fetch must be specified', 'add') + + # 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 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 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') + args.fetchuri = args.srctree + args.srctree = '' + elif args.recipename and not args.srctree: + if os.sep in args.recipename: + 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' % args.recipename) + + if not args.fetchuri: + if args.srcrev: + raise DevtoolError('The -S/--srcrev option is only valid when fetching from an SCM repository') + if args.srcbranch: + raise DevtoolError('The -B/--srcbranch option is only valid when fetching from an SCM repository') + + 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: + 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: + if args.recipename in workspace: + raise DevtoolError("recipe %s is already in your workspace" % + args.recipename) + reason = oe.recipeutils.validate_pn(args.recipename) + if reason: + raise DevtoolError(reason) + + if args.srctree: + srctree = os.path.abspath(args.srctree) + srctreeparent = None + tmpsrcdir = None + else: + srctree = None + srctreeparent = get_default_srctree(config) + bb.utils.mkdirhier(srctreeparent) + tmpsrcdir = tempfile.mkdtemp(prefix='devtoolsrc', dir=srctreeparent) + + if srctree and os.path.exists(srctree): + if args.fetchuri: + if not os.path.isdir(srctree): + raise DevtoolError("Cannot fetch into source tree path %s as " + "it exists and is not a directory" % + srctree) + elif os.listdir(srctree): + raise DevtoolError("Cannot fetch into source tree path %s as " + "it already exists and is non-empty" % + srctree) + elif not args.fetchuri: + if args.srctree: + raise DevtoolError("Specified source tree %s could not be found" % + args.srctree) + elif srctree: + raise DevtoolError("No source tree exists at default path %s - " + "either create and populate this directory, " + "or specify a path to a source tree, or a " + "URI to fetch source from" % srctree) + else: + raise DevtoolError("You must either specify a source tree " + "or a URI to fetch source from") + + if args.version: + if '_' in args.version or ' ' in args.version: + raise DevtoolError('Invalid version string "%s"' % args.version) + + if args.color == 'auto' and sys.stdout.isatty(): + color = 'always' + else: + color = args.color + extracmdopts = '' + if args.fetchuri: + source = args.fetchuri + if srctree: + extracmdopts += ' -x %s' % srctree + else: + extracmdopts += ' -x %s' % tmpsrcdir + else: + source = srctree + if args.recipename: + extracmdopts += ' -N %s' % args.recipename + if args.version: + extracmdopts += ' -V %s' % args.version + if args.binary: + extracmdopts += ' -b' + if args.also_native: + 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' + if args.mirrors: + extracmdopts += ' --mirrors' + if args.srcrev: + extracmdopts += ' --srcrev %s' % args.srcrev + if args.srcbranch: + extracmdopts += ' --srcbranch %s' % args.srcbranch + if args.provides: + extracmdopts += ' --provides %s' % args.provides + + tempdir = tempfile.mkdtemp(prefix='devtool') + try: + 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 == 15: + raise DevtoolError('Could not auto-determine recipe name, please specify it on the command line') + else: + raise DevtoolError('Command \'%s\' failed' % e.command) + + recipes = glob.glob(os.path.join(tempdir, '*.bb')) + if recipes: + recipename = os.path.splitext(os.path.basename(recipes[0]))[0].split('_')[0] + if recipename in workspace: + raise DevtoolError('A recipe with the same name as the one being created (%s) already exists in your workspace' % recipename) + recipedir = os.path.join(config.workspace_path, 'recipes', recipename) + bb.utils.mkdirhier(recipedir) + recipefile = os.path.join(recipedir, os.path.basename(recipes[0])) + appendfile = recipe_to_append(recipefile, config) + if os.path.exists(appendfile): + # This shouldn't be possible, but just in case + raise DevtoolError('A recipe with the same name as the one being created already exists in your workspace') + if os.path.exists(recipefile): + raise DevtoolError('A recipe file %s already exists in your workspace; this shouldn\'t be there - please delete it before continuing' % recipefile) + if tmpsrcdir: + srctree = os.path.join(srctreeparent, recipename) + if os.path.exists(tmpsrcdir): + if os.path.exists(srctree): + if os.path.isdir(srctree): + try: + os.rmdir(srctree) + except OSError as e: + if e.errno == errno.ENOTEMPTY: + raise DevtoolError('Source tree path %s already exists and is not empty' % srctree) + else: + raise + else: + raise DevtoolError('Source tree path %s already exists and is not a directory' % srctree) + logger.info('Using default source tree path %s' % srctree) + shutil.move(tmpsrcdir, srctree) + else: + 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', 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: + if tmpsrcdir and os.path.exists(tmpsrcdir): + shutil.rmtree(tmpsrcdir) + shutil.rmtree(tempdir) + + for fn in os.listdir(recipedir): + _add_md5(config, recipename, os.path.join(recipedir, fn)) + + tinfoil = setup_tinfoil(config_only=True, basepath=basepath) + 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 + + if args.fetchuri and not args.no_git: + setup_git_repo(srctree, args.version, 'devtool', d=tinfoil.config_data) + + 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') + + # Check if the new layer provides recipes whose priorities have been + # overriden by PREFERRED_PROVIDER. + recipe_name = rd.getVar('PN') + provides = rd.getVar('PROVIDES') + # Search every item defined in PROVIDES + for recipe_provided in provides.split(): + preferred_provider = 'PREFERRED_PROVIDER_' + recipe_provided + current_pprovider = rd.getVar(preferred_provider) + if current_pprovider and current_pprovider != recipe_name: + if args.fixed_setup: + #if we are inside the eSDK add the new PREFERRED_PROVIDER in the workspace layer.conf + layerconf_file = os.path.join(config.workspace_path, "conf", "layer.conf") + with open(layerconf_file, 'a') as f: + f.write('%s = "%s"\n' % (preferred_provider, recipe_name)) + else: + logger.warn('Set \'%s\' in order to use the recipe' % preferred_provider) + break + + _add_md5(config, recipename, appendfile) + + check_prerelease_version(rd.getVar('PV'), 'devtool add') + + logger.info('Recipe %s has been automatically created; further editing may be required to make it fully functional' % recipefile) + + finally: + tinfoil.shutdown() + + return 0 + + +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", + 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, 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, 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, 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, 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, 4) + + 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 " + "first." % pn) + +def _dry_run_copy(src, dst, dry_run_outdir, base_outdir): + """Common function for copying a file to the dry run output directory""" + relpath = os.path.relpath(dst, base_outdir) + if relpath.startswith('..'): + raise Exception('Incorrect base path %s for path %s' % (base_outdir, dst)) + dst = os.path.join(dry_run_outdir, relpath) + dst_d = os.path.dirname(dst) + if dst_d: + bb.utils.mkdirhier(dst_d) + # Don't overwrite existing files, otherwise in the case of an upgrade + # the dry-run written out recipe will be overwritten with an unmodified + # version + if not os.path.exists(dst): + shutil.copy(src, dst) + +def _move_file(src, dst, dry_run_outdir=None, base_outdir=None): + """Move a file. Creates all the directory components of destination path.""" + dry_run_suffix = ' (dry-run)' if dry_run_outdir else '' + logger.debug('Moving %s to %s%s' % (src, dst, dry_run_suffix)) + if dry_run_outdir: + # We want to copy here, not move + _dry_run_copy(src, dst, dry_run_outdir, base_outdir) + else: + dst_d = os.path.dirname(dst) + if dst_d: + bb.utils.mkdirhier(dst_d) + shutil.move(src, dst) + +def _copy_file(src, dst, dry_run_outdir=None): + """Copy a file. Creates all the directory components of destination path.""" + dry_run_suffix = ' (dry-run)' if dry_run_outdir else '' + logger.debug('Copying %s to %s%s' % (src, dst, dry_run_suffix)) + if dry_run_outdir: + _dry_run_copy(src, dst, dry_run_outdir, base_outdir) + else: + 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 + cmd = ['git', 'ls-tree', '-z', treeish] + if recursive: + cmd.append('-r') + out, _ = bb.process.run(cmd, cwd=repodir) + ret = {} + 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): + """Return pathspec (list of paths) that excludes certain path""" + # NOTE: "Filtering out" files/paths in this way is not entirely reliable - + # we don't catch files that are deleted, for example. A more reliable way + # to implement this would be to use "negative pathspecs" which were + # introduced in Git v1.9.0. Revisit this when/if the required Git version + # becomes greater than that. + path = os.path.normpath(path) + recurse = True if len(path.split(os.path.sep)) > 1 else False + git_files = list(_git_ls_tree(srctree, 'HEAD', recurse).keys()) + if path in git_files: + git_files.remove(path) + return git_files + else: + return ['.'] + +def _ls_tree(directory): + """Recursive listing of files in a directory""" + ret = [] + for root, dirs, files in os.walk(directory): + ret.extend([os.path.relpath(os.path.join(root, fname), directory) for + fname in files]) + return ret + + +def extract(args, config, basepath, workspace): + """Entry point for the devtool 'extract' subcommand""" + import bb + + tinfoil = setup_tinfoil(basepath=basepath, tracking=True) + if not tinfoil: + # Error already shown + return 1 + try: + 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, config, basepath, workspace, args.fixed_setup, rd, tinfoil, no_overrides=args.no_overrides) + logger.info('Source tree extracted to %s' % srctree) + + if initial_rev: + return 0 + else: + return 1 + finally: + tinfoil.shutdown() + +def sync(args, config, basepath, workspace): + """Entry point for the devtool 'sync' subcommand""" + import bb + + tinfoil = setup_tinfoil(basepath=basepath, tracking=True) + if not tinfoil: + # Error already shown + return 1 + try: + 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, config, basepath, workspace, args.fixed_setup, rd, tinfoil, no_overrides=True) + logger.info('Source tree %s synchronized' % srctree) + + if initial_rev: + return 0 + else: + return 1 + finally: + tinfoil.shutdown() + + +def _extract_source(srctree, keep_temp, devbranch, sync, config, basepath, workspace, fixed_setup, d, tinfoil, no_overrides=False): + """Extract sources of a recipe""" + import oe.recipeutils + import oe.patch + + pn = d.getVar('PN') + + _check_compatible_recipe(pn, d) + + if sync: + if not os.path.exists(srctree): + raise DevtoolError("output path %s does not exist" % srctree) + else: + if os.path.exists(srctree): + if not os.path.isdir(srctree): + raise DevtoolError("output path %s exists and is not a directory" % + srctree) + elif os.listdir(srctree): + raise DevtoolError("output path %s already exists and is " + "non-empty" % srctree) + + if 'noexec' in (d.getVarFlags('do_unpack', False) or []): + raise DevtoolError("The %s recipe has do_unpack disabled, unable to " + "extract source" % pn, 4) + + if not sync: + # Prepare for shutil.move later on + bb.utils.mkdirhier(srctree) + os.rmdir(srctree) + + extra_overrides = [] + if not no_overrides: + history = d.varhistory.variable('SRC_URI') + for event in history: + if not 'flag' in event: + if event['op'].startswith(('_append[', '_prepend[')): + extra_overrides.append(event['op'].split('[')[1].split(']')[0]) + if extra_overrides: + logger.info('SRC_URI contains some conditional appends/prepends - will create branches to represent these') + + initial_rev = None + + appendexisted = False + recipefile = d.getVar('FILE') + appendfile = recipe_to_append(recipefile, config) + is_kernel_yocto = bb.data.inherits_class('kernel-yocto', d) + + # 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) + + # FIXME this results in a cache reload under control of tinfoil, which is fine + # except we don't get the knotty progress bar + + if os.path.exists(appendfile): + appendbackup = os.path.join(tempdir, os.path.basename(appendfile) + '.bak') + shutil.copyfile(appendfile, appendbackup) + else: + appendbackup = None + bb.utils.mkdirhier(os.path.dirname(appendfile)) + logger.debug('writing append file %s' % appendfile) + with open(appendfile, 'a') as f: + f.write('###--- _extract_source\n') + f.write('DEVTOOL_TEMPDIR = "%s"\n' % tempdir) + f.write('DEVTOOL_DEVBRANCH = "%s"\n' % devbranch) + if not is_kernel_yocto: + f.write('PATCHTOOL = "git"\n') + f.write('PATCH_COMMIT_FUNCTIONS = "1"\n') + if extra_overrides: + f.write('DEVTOOL_EXTRA_OVERRIDES = "%s"\n' % ':'.join(extra_overrides)) + f.write('inherit devtool-source\n') + f.write('###--- _extract_source\n') + + update_unlockedsigs(basepath, workspace, fixed_setup, [pn]) + + sstate_manifests = d.getVar('SSTATE_MANIFESTS') + bb.utils.mkdirhier(sstate_manifests) + preservestampfile = os.path.join(sstate_manifests, 'preserve-stamps') + with open(preservestampfile, 'w') as f: + f.write(d.getVar('STAMP')) + try: + if bb.data.inherits_class('kernel-yocto', d): + # We need to generate the kernel config + task = 'do_configure' + else: + task = 'do_patch' + + # Run the fetch + unpack tasks + res = tinfoil.build_targets(pn, + task, + handle_events=True) + finally: + if os.path.exists(preservestampfile): + os.remove(preservestampfile) + + if not res: + raise DevtoolError('Extracting source for %s failed' % pn) + + try: + with open(os.path.join(tempdir, 'initial_rev'), 'r') as f: + initial_rev = f.read() + + with open(os.path.join(tempdir, 'srcsubdir'), 'r') as f: + srcsubdir = f.read() + except FileNotFoundError as e: + raise DevtoolError('Something went wrong with source extraction - the devtool-source class was not active or did not function correctly:\n%s' % str(e)) + srcsubdir_rel = os.path.relpath(srcsubdir, os.path.join(tempdir, 'workdir')) + + tempdir_localdir = os.path.join(tempdir, 'oe-local-files') + srctree_localdir = os.path.join(srctree, 'oe-local-files') + + if sync: + bb.process.run('git fetch file://' + srcsubdir + ' ' + devbranch + ':' + devbranch, cwd=srctree) + + # Move oe-local-files directory to srctree + # As the oe-local-files is not part of the constructed git tree, + # remove them directly during the synchrounizating might surprise + # the users. Instead, we move it to oe-local-files.bak and remind + # user in the log message. + if os.path.exists(srctree_localdir + '.bak'): + shutil.rmtree(srctree_localdir, srctree_localdir + '.bak') + + if os.path.exists(srctree_localdir): + logger.info('Backing up current local file directory %s' % srctree_localdir) + shutil.move(srctree_localdir, srctree_localdir + '.bak') + + if os.path.exists(tempdir_localdir): + logger.info('Syncing local source files to srctree...') + shutil.copytree(tempdir_localdir, srctree_localdir) + else: + # Move oe-local-files directory to srctree + if os.path.exists(tempdir_localdir): + logger.info('Adding local source files to srctree...') + shutil.move(tempdir_localdir, srcsubdir) + + shutil.move(srcsubdir, srctree) + + 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 is_kernel_yocto: + logger.info('Copying kernel config to srctree') + shutil.copy2(os.path.join(tempdir, '.config'), srctree) + + finally: + if appendbackup: + shutil.copyfile(appendbackup, appendfile) + elif os.path.exists(appendfile): + os.remove(appendfile) + if keep_temp: + logger.info('Preserving temporary directory %s' % tempdir) + else: + shutil.rmtree(tempdir) + return initial_rev, srcsubdir_rel + +def _add_md5(config, recipename, filename): + """Record checksum of a file (or recursively for a directory) to the md5-file of the workspace""" + import bb.utils + + def addfile(fn): + md5 = bb.utils.md5_file(fn) + with open(os.path.join(config.workspace_path, '.devtool_md5'), 'a+') as f: + md5_str = '%s|%s|%s\n' % (recipename, os.path.relpath(fn, config.workspace_path), md5) + f.seek(0, os.SEEK_SET) + if not md5_str in f.read(): + f.write(md5_str) + + if os.path.isdir(filename): + for root, _, files in os.walk(filename): + for f in files: + addfile(os.path.join(root, f)) + else: + addfile(filename) + +def _check_preserve(config, recipename): + """Check if a file was manually changed and needs to be saved in 'attic' + directory""" + 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', recipename) + with open(origfile, 'r') as f: + with open(newfile, 'w') as tf: + for line in f.readlines(): + splitline = line.rstrip().split('|') + if splitline[0] == recipename: + removefile = os.path.join(config.workspace_path, splitline[1]) + try: + md5 = bb.utils.md5_file(removefile) + except IOError as err: + if err.errno == 2: + # File no longer exists, skip it + continue + else: + raise + if splitline[2] != md5: + bb.utils.mkdirhier(preservepath) + preservefile = os.path.basename(removefile) + logger.warn('File %s modified since it was written, preserving in %s' % (preservefile, preservepath)) + shutil.move(removefile, os.path.join(preservepath, preservefile)) + else: + os.remove(removefile) + else: + tf.write(line) + os.rename(newfile, origfile) + +def modify(args, config, basepath, workspace): + """Entry point for the devtool 'modify' subcommand""" + import bb + import oe.recipeutils + import oe.patch + + if args.recipename in workspace: + raise DevtoolError("recipe %s is already in your workspace" % + args.recipename) + + tinfoil = setup_tinfoil(basepath=basepath, tracking=True) + try: + 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 = 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) + + 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 = [] + check_commits = False + if not args.no_extract: + initial_rev, _ = _extract_source(srctree, args.keep_temp, args.branch, False, config, basepath, workspace, args.fixed_setup, rd, tinfoil, no_overrides=args.no_overrides) + 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() + check_commits = True + 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 = '' + if stdout: + check_commits = True + 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() + + branch_patches = {} + if check_commits: + # Check if there are override branches + (stdout, _) = bb.process.run('git branch', cwd=srctree) + branches = [] + for line in stdout.rstrip().splitlines(): + branchname = line[2:].rstrip() + if branchname.startswith(override_branch_prefix): + branches.append(branchname) + if branches: + logger.warn('SRC_URI is conditionally overridden in this recipe, thus several %s* branches have been created, one for each override that makes changes to SRC_URI. It is recommended that you make changes to the %s branch first, then checkout and rebase each %s* branch and update any unique patches there (duplicates on those branches will be ignored by devtool finish/update-recipe)' % (override_branch_prefix, args.branch, override_branch_prefix)) + branches.insert(0, args.branch) + seen_patches = [] + for branch in branches: + branch_patches[branch] = [] + (stdout, _) = bb.process.run('git log devtool-base..%s' % branch, cwd=srctree) + for line in stdout.splitlines(): + line = line.strip() + if line.startswith(oe.patch.GitApplyTree.patch_line_prefix): + origpatch = line[len(oe.patch.GitApplyTree.patch_line_prefix):].split(':', 1)[-1].strip() + if not origpatch in seen_patches: + seen_patches.append(origpatch) + branch_patches[branch].append(origpatch) + + # Need to grab this here in case the source is within a subdirectory + srctreebase = srctree + + # 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(srctreebase, 'oe-local-files')) + f.write('# srctreebase: %s\n' % srctreebase) + + 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_kernel_configme do_kernel_configcheck"\n') + f.write('\ndo_patch() {\n' + ' :\n' + '}\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) + if branch_patches: + for branch in branch_patches: + if branch == args.branch: + continue + f.write('# patches_%s: %s\n' % (branch, ','.join(branch_patches[branch]))) + + update_unlockedsigs(basepath, workspace, args.fixed_setup, [pn]) + + _add_md5(config, pn, appendfile) + + logger.info('Recipe %s now set up to build from %s' % (pn, srctree)) + + finally: + tinfoil.shutdown() + + 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: + 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() + + if args.version: + newver = args.version + else: + newver = origfnver + + 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(srctree, recipe_path, initial_rev=None, force_patch_refresh=False): + """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 + + # Get current branch + stdout, _ = bb.process.run('git rev-parse --abbrev-ref HEAD', + cwd=srctree) + branchname = stdout.rstrip() + + # Parse initial rev from recipe if not specified + commits = [] + patches = [] + with open(recipe_path, 'r') as f: + for line in f: + if line.startswith('# initial_rev:'): + if not initial_rev: + initial_rev = line.split(':')[-1].strip() + elif line.startswith('# commit:') and not force_patch_refresh: + commits.append(line.split(':')[-1].strip()) + elif line.startswith('# patches_%s:' % branchname): + patches = line.split(':')[-1].strip().split(',') + + update_rev = initial_rev + changed_revs = None + if initial_rev: + # Find first actually changed revision + stdout, _ = bb.process.run('git rev-list --reverse %s..HEAD' % + initial_rev, cwd=srctree) + newcommits = stdout.split() + for i in range(min(len(commits), len(newcommits))): + if newcommits[i] == commits[i]: + update_rev = commits[i] + + try: + stdout, _ = bb.process.run('git cherry devtool-patched', + cwd=srctree) + except bb.process.ExecutionError as err: + stdout = None + + if stdout is not None and not force_patch_refresh: + changed_revs = [] + for line in stdout.splitlines(): + if line.startswith('+ '): + rev = line.split()[1] + if rev in newcommits: + changed_revs.append(rev) + + return initial_rev, update_rev, changed_revs, patches + +def _remove_file_entries(srcuri, filelist): + """Remove file:// entries from SRC_URI""" + remaining = filelist[:] + entries = [] + for fname in filelist: + basename = os.path.basename(fname) + for i in range(len(srcuri)): + if (srcuri[i].startswith('file://') and + os.path.basename(srcuri[i].split(';')[0]) == basename): + entries.append(srcuri[i]) + remaining.remove(fname) + srcuri.pop(i) + break + return entries, remaining + +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, no_report_remove=False, dry_run=False): + """Unlink existing patch files""" + + dry_run_suffix = ' (dry-run)' if dry_run else '' + + for path in files: + if append: + if not destpath: + raise Exception('destpath should be set here') + path = os.path.join(destpath, os.path.basename(path)) + + if os.path.exists(path): + if not no_report_remove: + logger.info('Removing file %s%s' % (path, dry_run_suffix)) + if not dry_run: + # FIXME "git rm" here would be nice if the file in question is + # tracked + # FIXME there's a chance that this file is referred to by + # another recipe, in which case deleting wouldn't be the + # right thing to do + os.remove(path) + # Remove directory if empty + try: + os.rmdir(os.path.dirname(path)) + except OSError as ose: + if ose.errno != errno.ENOTEMPTY: + raise + + +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 + 2. added - new patches that don't exist in SRCURI + 3 removed - patches that exist in SRCURI but not in exported patches + In each dict the key is the 'basepath' of the URI and value is the + absolute path to the existing file in recipe space (if any). + """ + import oe.recipeutils + from oe.patch import GitApplyTree + updated = OrderedDict() + added = OrderedDict() + seqpatch_re = re.compile('^([0-9]{4}-)?(.+)') + + existing_patches = dict((os.path.basename(path), path) for path in + oe.recipeutils.get_recipe_patches(rd)) + logger.debug('Existing patches: %s' % existing_patches) + + # Generate patches from Git, exclude local files directory + patch_pathspec = _git_exclude_path(srctree, 'oe-local-files') + GitApplyTree.extractPatches(srctree, start_rev, destdir, patch_pathspec) + + new_patches = sorted(os.listdir(destdir)) + for new_patch in new_patches: + # Strip numbering from patch names. If it's a git sequence named patch, + # the numbers might not match up since we are starting from a different + # 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) + match_name = None + for old_patch in existing_patches: + old_basename = seqpatch_re.match(old_patch).group(2) + 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 + 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, srctreebase): + """Copy local files from srctree to given location. + Returns three-tuple of dicts: + 1. updated - files that already exist in SRCURI + 2. added - new files files that don't exist in SRCURI + 3 removed - files that exist in SRCURI but not in exported files + In each dict the key is the 'basepath' of the URI and value is the + absolute path to the existing file in recipe space (if any). + """ + import oe.recipeutils + + # Find out local files (SRC_URI files that exist in the "recipe space"). + # Local files that reside in srctree are not included in patch generation. + # Instead they are directly copied over the original source files (in + # recipe space). + existing_files = oe.recipeutils.get_recipe_local_files(rd) + new_set = None + updated = OrderedDict() + added = OrderedDict() + removed = OrderedDict() + local_files_dir = os.path.join(srctreebase, '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 + # the tree object of the directory + tmp_index = os.path.join(srctree, '.git', 'index.tmp.devtool') + tree = git_files['oe-local-files'][2] + bb.process.run(['git', 'checkout', tree, '--', '.'], cwd=srctree, + env=dict(os.environ, GIT_WORK_TREE=destdir, + GIT_INDEX_FILE=tmp_index)) + 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(local_files_dir) + bb.process.run(['cp', '-ax', + os.path.join(local_files_dir, '.'), 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: + 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 _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(recipename, workspace, srctree, rd, appendlayerdir, wildcard_version, no_remove, no_report_remove, dry_run_outdir=None): + """Implement the 'srcrev' mode of update-recipe""" + import bb + import oe.recipeutils + + dry_run_suffix = ' (dry-run)' if dry_run_outdir else '' + + recipefile = rd.getVar('FILE') + recipedir = os.path.basename(recipefile) + logger.info('Updating SRCREV in recipe %s%s' % (recipedir, dry_run_suffix)) + + # Get HEAD revision + try: + stdout, _ = bb.process.run('git rev-parse HEAD', cwd=srctree) + except bb.process.ExecutionError as err: + raise DevtoolError('Failed to get HEAD revision in %s: %s' % + (srctree, err)) + srcrev = stdout.strip() + if len(srcrev) != 40: + raise DevtoolError('Invalid hash returned by git: %s' % stdout) + + destpath = None + remove_files = [] + patchfields = {} + patchfields['SRCREV'] = srcrev + orig_src_uri = rd.getVar('SRC_URI', False) or '' + srcuri = orig_src_uri.split() + tempdir = tempfile.mkdtemp(prefix='devtool') + update_srcuri = False + appendfile = None + try: + local_files_dir = tempfile.mkdtemp(dir=tempdir) + srctreebase = workspace[recipename]['srctreebase'] + upd_f, new_f, del_f = _export_local_files(srctree, rd, local_files_dir, srctreebase) + if not no_remove: + # Find list of existing patches in recipe file + patches_dir = tempfile.mkdtemp(dir=tempdir) + old_srcrev = rd.getVar('SRCREV') or '' + upd_p, new_p, del_p = _export_patches(srctree, rd, old_srcrev, + patches_dir) + logger.debug('Patches: update %s, new %s, delete %s' % (dict(upd_p), dict(new_p), dict(del_p))) + + # Remove deleted local files and "overlapping" patches + remove_files = list(del_f.values()) + list(upd_p.values()) + list(del_p.values()) + if remove_files: + removedentries = _remove_file_entries(srcuri, remove_files)[0] + update_srcuri = True + + if appendlayerdir: + files = dict((os.path.join(local_files_dir, key), val) for + 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) + if dry_run_outdir: + logger.info('Creating bbappend (dry-run)') + else: + appendfile, destpath = oe.recipeutils.bbappend_recipe( + rd, appendlayerdir, files, wildcardver=wildcard_version, + extralines=patchfields, removevalues=removevalues, + redirect_output=dry_run_outdir) + else: + files_dir = _determine_files_dir(rd) + for basepath, path in upd_f.items(): + logger.info('Updating file %s%s' % (basepath, dry_run_suffix)) + 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, dry_run_outdir=dry_run_outdir, base_outdir=recipedir) + else: + _move_file(os.path.join(local_files_dir, basepath), path, + dry_run_outdir=dry_run_outdir, base_outdir=recipedir) + update_srcuri= True + for basepath, path in new_f.items(): + logger.info('Adding new file %s%s' % (basepath, dry_run_suffix)) + _move_file(os.path.join(local_files_dir, basepath), + os.path.join(files_dir, basepath), + dry_run_outdir=dry_run_outdir, + base_outdir=recipedir) + srcuri.append('file://%s' % basepath) + update_srcuri = True + if update_srcuri: + patchfields['SRC_URI'] = ' '.join(srcuri) + ret = oe.recipeutils.patch_recipe(rd, recipefile, patchfields, redirect_output=dry_run_outdir) + finally: + shutil.rmtree(tempdir) + if not 'git://' in orig_src_uri: + logger.info('You will need to update SRC_URI within the recipe to ' + 'point to a git repository where you have pushed your ' + 'changes') + + _remove_source_files(appendlayerdir, remove_files, destpath, no_report_remove, dry_run=dry_run_outdir) + return True, appendfile, remove_files + +def _update_recipe_patch(recipename, workspace, srctree, rd, appendlayerdir, wildcard_version, no_remove, no_report_remove, initial_rev, dry_run_outdir=None, force_patch_refresh=False): + """Implement the 'patch' mode of update-recipe""" + import bb + import oe.recipeutils + + recipefile = rd.getVar('FILE') + recipedir = os.path.dirname(recipefile) + append = workspace[recipename]['bbappend'] + if not os.path.exists(append): + raise DevtoolError('unable to find workspace bbappend for recipe %s' % + recipename) + + initial_rev, update_rev, changed_revs, filter_patches = _get_patchset_revs(srctree, append, initial_rev, force_patch_refresh) + if not initial_rev: + raise DevtoolError('Unable to find initial revision - please specify ' + 'it with --initial-rev') + + appendfile = None + dl_dir = rd.getVar('DL_DIR') + if not dl_dir.endswith('/'): + dl_dir += '/' + + dry_run_suffix = ' (dry-run)' if dry_run_outdir else '' + + tempdir = tempfile.mkdtemp(prefix='devtool') + try: + local_files_dir = tempfile.mkdtemp(dir=tempdir) + if filter_patches: + upd_f = {} + new_f = {} + del_f = {} + else: + srctreebase = workspace[recipename]['srctreebase'] + upd_f, new_f, del_f = _export_local_files(srctree, rd, local_files_dir, srctreebase) + + remove_files = [] + if not no_remove: + # Get all patches from source tree and check if any should be removed + all_patches_dir = tempfile.mkdtemp(dir=tempdir) + _, _, del_p = _export_patches(srctree, rd, initial_rev, + all_patches_dir) + # Remove deleted local files and patches + 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, _ = _export_patches(srctree, rd, update_rev, + patches_dir, changed_revs) + logger.debug('Pre-filtering: update: %s, new: %s' % (dict(upd_p), dict(new_p))) + if filter_patches: + new_p = {} + upd_p = {k:v for k,v in upd_p.items() if k in filter_patches} + remove_files = [f for f in remove_files if f in filter_patches] + updatefiles = False + updaterecipe = False + destpath = None + srcuri = (rd.getVar('SRC_URI', False) or '').split() + if appendlayerdir: + files = dict((os.path.join(local_files_dir, key), val) for + 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 list(upd_p.items()) + list(new_p.items()))) + if files or remove_files: + removevalues = None + if remove_files: + removedentries, remaining = _remove_file_entries( + srcuri, remove_files) + if removedentries or remaining: + remaining = ['file://' + os.path.basename(item) for + item in remaining] + removevalues = {'SRC_URI': removedentries + remaining} + appendfile, destpath = oe.recipeutils.bbappend_recipe( + rd, appendlayerdir, files, + wildcardver=wildcard_version, + removevalues=removevalues, + redirect_output=dry_run_outdir) + else: + logger.info('No patches or local source files needed updating') + else: + # Update existing files + files_dir = _determine_files_dir(rd) + for basepath, path in upd_f.items(): + logger.info('Updating file %s' % basepath) + 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, + dry_run_outdir=dry_run_outdir, base_outdir=recipedir) + else: + _move_file(os.path.join(local_files_dir, basepath), path, + dry_run_outdir=dry_run_outdir, base_outdir=recipedir) + updatefiles = True + for basepath, path in upd_p.items(): + patchfn = os.path.join(patches_dir, 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%s' % (basepath, dry_run_suffix)) + _move_file(patchfn, path, + dry_run_outdir=dry_run_outdir, base_outdir=recipedir) + updatefiles = True + # Add any new files + for basepath, path in new_f.items(): + logger.info('Adding new file %s%s' % (basepath, dry_run_suffix)) + _move_file(os.path.join(local_files_dir, basepath), + os.path.join(files_dir, basepath), + dry_run_outdir=dry_run_outdir, + base_outdir=recipedir) + srcuri.append('file://%s' % basepath) + updaterecipe = True + for basepath, path in new_p.items(): + logger.info('Adding new patch %s%s' % (basepath, dry_run_suffix)) + _move_file(os.path.join(patches_dir, basepath), + os.path.join(files_dir, basepath), + dry_run_outdir=dry_run_outdir, + base_outdir=recipedir) + srcuri.append('file://%s' % basepath) + updaterecipe = True + # Update recipe, if needed + if _remove_file_entries(srcuri, remove_files)[0]: + updaterecipe = True + if updaterecipe: + if not dry_run_outdir: + logger.info('Updating recipe %s' % os.path.basename(recipefile)) + ret = oe.recipeutils.patch_recipe(rd, recipefile, + {'SRC_URI': ' '.join(srcuri)}, + redirect_output=dry_run_outdir) + elif not updatefiles: + # Neither patches nor recipe were updated + logger.info('No patches or files need updating') + return False, None, [] + finally: + shutil.rmtree(tempdir) + + _remove_source_files(appendlayerdir, remove_files, destpath, no_report_remove, dry_run=dry_run_outdir) + return True, appendfile, remove_files + +def _guess_recipe_update_mode(srctree, rdata): + """Guess the recipe update mode to use""" + src_uri = (rdata.getVar('SRC_URI', False) or '').split() + git_uris = [uri for uri in src_uri if uri.startswith('git://')] + if not git_uris: + return 'patch' + # Just use the first URI for now + uri = git_uris[0] + # Check remote branch + params = bb.fetch.decodeurl(uri)[5] + upstr_branch = params['branch'] if 'branch' in params else 'master' + # Check if current branch HEAD is found in upstream branch + stdout, _ = bb.process.run('git rev-parse HEAD', cwd=srctree) + head_rev = stdout.rstrip() + stdout, _ = bb.process.run('git branch -r --contains %s' % head_rev, + cwd=srctree) + remote_brs = [branch.strip() for branch in stdout.splitlines()] + if 'origin/' + upstr_branch in remote_brs: + return 'srcrev' + + return 'patch' + +def _update_recipe(recipename, workspace, rd, mode, appendlayerdir, wildcard_version, no_remove, initial_rev, no_report_remove=False, dry_run_outdir=None, no_overrides=False, force_patch_refresh=False): + srctree = workspace[recipename]['srctree'] + if mode == 'auto': + mode = _guess_recipe_update_mode(srctree, rd) + + override_branches = [] + mainbranch = None + startbranch = None + if not no_overrides: + stdout, _ = bb.process.run('git branch', cwd=srctree) + other_branches = [] + for line in stdout.splitlines(): + branchname = line[2:] + if line.startswith('* '): + startbranch = branchname + if branchname.startswith(override_branch_prefix): + override_branches.append(branchname) + else: + other_branches.append(branchname) + + if override_branches: + logger.debug('_update_recipe: override branches: %s' % override_branches) + logger.debug('_update_recipe: other branches: %s' % other_branches) + if startbranch.startswith(override_branch_prefix): + if len(other_branches) == 1: + mainbranch = other_branches[1] + else: + raise DevtoolError('Unable to determine main branch - please check out the main branch in source tree first') + else: + mainbranch = startbranch + + checkedout = None + anyupdated = False + appendfile = None + allremoved = [] + if override_branches: + logger.info('Handling main branch (%s)...' % mainbranch) + if startbranch != mainbranch: + bb.process.run('git checkout %s' % mainbranch, cwd=srctree) + checkedout = mainbranch + try: + branchlist = [mainbranch] + override_branches + for branch in branchlist: + crd = bb.data.createCopy(rd) + if branch != mainbranch: + logger.info('Handling branch %s...' % branch) + override = branch[len(override_branch_prefix):] + crd.appendVar('OVERRIDES', ':%s' % override) + bb.process.run('git checkout %s' % branch, cwd=srctree) + checkedout = branch + + if mode == 'srcrev': + updated, appendf, removed = _update_recipe_srcrev(recipename, workspace, srctree, crd, appendlayerdir, wildcard_version, no_remove, no_report_remove, dry_run_outdir) + elif mode == 'patch': + updated, appendf, removed = _update_recipe_patch(recipename, workspace, srctree, crd, appendlayerdir, wildcard_version, no_remove, no_report_remove, initial_rev, dry_run_outdir, force_patch_refresh) + else: + raise DevtoolError('update_recipe: invalid mode %s' % mode) + if updated: + anyupdated = True + if appendf: + appendfile = appendf + allremoved.extend(removed) + finally: + if startbranch and checkedout != startbranch: + bb.process.run('git checkout %s' % startbranch, cwd=srctree) + + return anyupdated, appendfile, allremoved + +def update_recipe(args, config, basepath, workspace): + """Entry point for the devtool 'update-recipe' subcommand""" + check_workspace_recipe(workspace, args.recipename) + + if args.append: + if not os.path.exists(args.append): + raise DevtoolError('bbappend destination layer directory "%s" ' + 'does not exist' % args.append) + if not os.path.exists(os.path.join(args.append, 'conf', 'layer.conf')): + raise DevtoolError('conf/layer.conf not found in bbappend ' + '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 + + dry_run_output = None + dry_run_outdir = None + if args.dry_run: + dry_run_output = tempfile.TemporaryDirectory(prefix='devtool') + dry_run_outdir = dry_run_output.name + updated, _, _ = _update_recipe(args.recipename, workspace, rd, args.mode, args.append, args.wildcard_version, args.no_remove, args.initial_rev, dry_run_outdir=dry_run_outdir, no_overrides=args.no_overrides, force_patch_refresh=args.force_patch_refresh) + + 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 + + +def status(args, config, basepath, workspace): + """Entry point for the devtool 'status' subcommand""" + if workspace: + for recipe, value in sorted(workspace.items()): + recipefile = value['recipefile'] + if recipefile: + recipestr = ' (%s)' % recipefile + else: + recipestr = '' + print("%s: %s%s" % (recipe, value['srctree'], recipestr)) + else: + logger.info('No recipes currently in your workspace - you can use "devtool modify" to work on an existing recipe or "devtool add" to add a new one') + return 0 + + +def _reset(recipes, no_clean, config, basepath, workspace): + """Reset one or more recipes""" + import oe.path + + def clean_preferred_provider(pn, layerconf_path): + """Remove PREFERRED_PROVIDER from layer.conf'""" + import re + layerconf_file = os.path.join(layerconf_path, 'conf', 'layer.conf') + new_layerconf_file = os.path.join(layerconf_path, 'conf', '.layer.conf') + pprovider_found = False + with open(layerconf_file, 'r') as f: + lines = f.readlines() + with open(new_layerconf_file, 'a') as nf: + for line in lines: + pprovider_exp = r'^PREFERRED_PROVIDER_.*? = "' + pn + r'"$' + if not re.match(pprovider_exp, line): + nf.write(line) + else: + pprovider_found = True + if pprovider_found: + shutil.move(new_layerconf_file, layerconf_file) + else: + os.remove(new_layerconf_file) + + if recipes and not no_clean: + if len(recipes) == 1: + logger.info('Cleaning sysroot for recipe %s...' % recipes[0]) + else: + logger.info('Cleaning sysroot for recipes %s...' % ', '.join(recipes)) + # If the recipe file itself was created in the workspace, and + # it uses BBCLASSEXTEND, then we need to also clean the other + # variants + targets = [] + for recipe in recipes: + targets.append(recipe) + recipefile = workspace[recipe]['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)) + except bb.process.ExecutionError as e: + raise DevtoolError('Command \'%s\' failed, output:\n%s\nIf you ' + 'wish, you may specify -n/--no-clean to ' + 'skip running this command when resetting' % + (e.command, e.stdout)) + + for pn in recipes: + _check_preserve(config, pn) + + appendfile = workspace[pn]['bbappend'] + if os.path.exists(appendfile): + # This shouldn't happen, but is possible if devtool errored out prior to + # writing the md5 file. We need to delete this here or the recipe won't + # actually be reset + os.remove(appendfile) + + 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): + for fn in files: + logger.warn('Preserving %s in %s' % (fn, preservepath)) + _move_file(os.path.join(origdir, fn), + os.path.join(preservepath, fn)) + for dn in dirs: + preservedir(os.path.join(root, dn)) + os.rmdir(origdir) + + recipefile = workspace[pn]['recipefile'] + if recipefile and oe.path.is_path_parent(config.workspace_path, recipefile): + # This should always be true if recipefile is set, but just in case + preservedir(os.path.dirname(recipefile)) + # We don't automatically create this dir next to appends, but the user can + preservedir(os.path.join(config.workspace_path, 'appends', pn)) + + srctreebase = workspace[pn]['srctreebase'] + if os.path.isdir(srctreebase): + if os.listdir(srctreebase): + # We don't want to risk wiping out any work in progress + logger.info('Leaving source tree %s as-is; if you no ' + 'longer need it then please delete it manually' + % srctreebase) + else: + # This is unlikely, but if it's empty we can just remove it + os.rmdir(srctreebase) + + clean_preferred_provider(pn, config.workspace_path) + +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) + return os.path.abspath(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) + + dry_run_suffix = ' (dry-run)' if args.dry_run else '' + + # Grab the equivalent of COREBASE without having to initialise tinfoil + corebasedir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..')) + + srctree = workspace[args.recipename]['srctree'] + check_git_repo_op(srctree, [corebasedir]) + dirty = check_git_repo_dirty(srctree) + if dirty: + if args.force: + logger.warning('Source tree is not clean, continuing as requested by -f/--force') + else: + raise DevtoolError('Source tree is not clean:\n\n%s\nEnsure you have committed your changes or use -f/--force if you are sure there\'s nothing that needs to be committed' % dirty) + + 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) + recipefile = rd.getVar('FILE') + recipedir = os.path.dirname(recipefile) + origlayerdir = oe.recipeutils.find_layerdir(recipefile) + + 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() + + destlayerbasedir = oe.recipeutils.find_layerdir(destlayerdir) + + 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(destlayerbasedir) 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 + + # Actually update the recipe / bbappend + removing_original = (origpath and origfilelist and oe.recipeutils.find_layerdir(origpath) == destlayerbasedir) + dry_run_output = None + dry_run_outdir = None + if args.dry_run: + dry_run_output = tempfile.TemporaryDirectory(prefix='devtool') + dry_run_outdir = dry_run_output.name + updated, appendfile, removed = _update_recipe(args.recipename, workspace, rd, args.mode, appendlayerdir, wildcard_version=True, no_remove=False, no_report_remove=removing_original, initial_rev=args.initial_rev, dry_run_outdir=dry_run_outdir, no_overrides=args.no_overrides, force_patch_refresh=args.force_patch_refresh) + removed = [os.path.relpath(pth, recipedir) for pth in removed] + + # Remove any old files in the case of an upgrade + if removing_original: + for fn in origfilelist: + fnp = os.path.join(origpath, fn) + if fn in removed or not os.path.exists(os.path.join(recipedir, fn)): + logger.info('Removing file %s%s' % (fnp, dry_run_suffix)) + if not args.dry_run: + try: + os.remove(fnp) + except FileNotFoundError: + pass + + 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%s' % (destpath, dry_run_suffix)) + 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)) + destfp = os.path.join(destdir, fn) + _move_file(srcpath, destfp, dry_run_outdir=dry_run_outdir, base_outdir=destpath) + + if dry_run_outdir: + import difflib + comparelist = [] + for root, _, files in os.walk(dry_run_outdir): + for fn in files: + outf = os.path.join(root, fn) + relf = os.path.relpath(outf, dry_run_outdir) + logger.debug('dry-run: output file %s' % relf) + if fn.endswith('.bb'): + if origfilelist and origpath and destpath: + # Need to match this up with the pre-upgrade recipe file + for origf in origfilelist: + if origf.endswith('.bb'): + comparelist.append((os.path.abspath(os.path.join(origpath, origf)), + outf, + os.path.abspath(os.path.join(destpath, relf)))) + break + else: + # Compare to the existing recipe + comparelist.append((recipefile, outf, recipefile)) + elif fn.endswith('.bbappend'): + if appendfile: + if os.path.exists(appendfile): + comparelist.append((appendfile, outf, appendfile)) + else: + comparelist.append((None, outf, appendfile)) + else: + if destpath: + recipedest = destpath + elif appendfile: + recipedest = os.path.dirname(appendfile) + else: + recipedest = os.path.dirname(recipefile) + destfp = os.path.join(recipedest, relf) + if os.path.exists(destfp): + comparelist.append((destfp, outf, destfp)) + output = '' + for oldfile, newfile, newfileshow in comparelist: + if oldfile: + with open(oldfile, 'r') as f: + oldlines = f.readlines() + else: + oldfile = '/dev/null' + oldlines = [] + with open(newfile, 'r') as f: + newlines = f.readlines() + if not newfileshow: + newfileshow = newfile + diff = difflib.unified_diff(oldlines, newlines, oldfile, newfileshow) + difflines = list(diff) + if difflines: + output += ''.join(difflines) + if output: + logger.info('Diff of changed files:\n%s' % output) + finally: + tinfoil.shutdown() + + # Everything else has succeeded, we can now reset + if args.dry_run: + logger.info('Resetting recipe (dry-run)') + else: + _reset([args.recipename], no_clean=no_clean, config=config, basepath=basepath, workspace=workspace) + + return 0 + + +def get_default_srctree(config, recipename=''): + """Get the default srctree path""" + srctreeparent = config.get('General', 'default_source_parent_dir', config.workspace_path) + if recipename: + return os.path.join(srctreeparent, 'sources', recipename) + else: + return os.path.join(srctreeparent, 'sources') + +def register_commands(subparsers, context): + """Register devtool subcommands from this plugin""" + + 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.', + 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') + group = parser_add.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_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") + group = parser_add.add_mutually_exclusive_group() + group.add_argument('--srcrev', '-S', help='Source revision to fetch if fetching from an SCM such as git (default latest)') + group.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('--srcbranch', '-B', help='Branch in source repository if fetching from an SCM such as git (default master)') + 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.add_argument('--mirrors', help='Enable PREMIRRORS and MIRRORS for source tree fetching (disable by default).', action="store_true") + parser_add.add_argument('--provides', '-p', help='Specify an alias for the item provided by the recipe. E.g. virtual/libgl') + 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='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') + group = parser_modify.add_mutually_exclusive_group() + group.add_argument('--extract', '-x', action="store_true", help='Extract source for recipe (default)') + group.add_argument('--no-extract', '-n', action="store_true", help='Do not extract source, expect it to exist') + 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 (when not using -n/--no-extract) (default "%(default)s")') + parser_modify.add_argument('--no-overrides', '-O', action="store_true", help='Do not create branches for other override configurations') + parser_modify.add_argument('--keep-temp', help='Keep temporary directory (for debugging)', action="store_true") + parser_modify.set_defaults(func=modify, fixed_setup=context.fixed_setup) + + parser_extract = subparsers.add_parser('extract', help='Extract 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")') + parser_extract.add_argument('--no-overrides', '-O', action="store_true", help='Do not create branches for other override configurations') + parser_extract.add_argument('--keep-temp', action="store_true", help='Keep temporary directory (for debugging)') + parser_extract.set_defaults(func=extract, fixed_setup=context.fixed_setup) + + 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, + 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, fixed_setup=context.fixed_setup) + + 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.', + 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') + parser_update_recipe.add_argument('--append', '-a', help='Write changes to a bbappend in the specified layer instead of the recipe', metavar='LAYERDIR') + parser_update_recipe.add_argument('--wildcard-version', '-w', help='In conjunction with -a/--append, use a wildcard to make the bbappend apply to any recipe version', action='store_true') + parser_update_recipe.add_argument('--no-remove', '-n', action="store_true", help='Don\'t remove patches, only add or update') + parser_update_recipe.add_argument('--no-overrides', '-O', action="store_true", help='Do not handle other override branches (if they exist)') + parser_update_recipe.add_argument('--dry-run', '-N', action="store_true", help='Dry-run (just report changes instead of writing them)') + parser_update_recipe.add_argument('--force-patch-refresh', action="store_true", help='Update patches in the layer even if they have not been modified (useful for refreshing patch 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', + 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(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. Note that your changes must have been committed to the git repository in order to be recognised.', + 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.add_argument('--force', '-f', action="store_true", help='Force continuing even if there are uncommitted changes in the source tree repository') + parser_finish.add_argument('--no-overrides', '-O', action="store_true", help='Do not handle other override branches (if they exist)') + parser_finish.add_argument('--dry-run', '-N', action="store_true", help='Dry-run (just report changes instead of writing them)') + parser_finish.add_argument('--force-patch-refresh', action="store_true", help='Update patches in the layer even if they have not been modified (useful for refreshing patch context)') + parser_finish.set_defaults(func=finish) diff --git a/poky/scripts/lib/devtool/upgrade.py b/poky/scripts/lib/devtool/upgrade.py new file mode 100644 index 000000000..c3fd866ef --- /dev/null +++ b/poky/scripts/lib/devtool/upgrade.py @@ -0,0 +1,626 @@ +# Development tool - upgrade command plugin +# +# Copyright (C) 2014-2017 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 upgrade plugin""" + +import os +import sys +import re +import shutil +import tempfile +import logging +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, update_unlockedsigs, check_prerelease_version + +logger = logging.getLogger('devtool') + +def _run(cmd, cwd=''): + logger.debug("Running command %s> %s" % (cwd,cmd)) + return bb.process.run('%s' % cmd, cwd=cwd) + +def _get_srctree(tmpdir): + srctree = tmpdir + dirs = os.listdir(tmpdir) + if len(dirs) == 1: + srctree = os.path.join(tmpdir, dirs[0]) + return srctree + +def _copy_source_code(orig, dest): + for path in standard._ls_tree(orig): + dest_dir = os.path.join(dest, os.path.dirname(path)) + bb.utils.mkdirhier(dest_dir) + dest_path = os.path.join(dest, path) + shutil.move(os.path.join(orig, path), dest_path) + +def _remove_patch_dirs(recipefolder): + for root, dirs, files in os.walk(recipefolder): + for d in dirs: + shutil.rmtree(os.path.join(root,d)) + +def _recipe_contains(rd, var): + rf = rd.getVar('FILE') + varfiles = oe.recipeutils.get_var_files(rf, [var], rd) + 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: + 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: + shutil.move(os.path.join(path, oldrecipe), os.path.join(path, newrecipe)) + else: + newrecipe = oldrecipe + return os.path.join(path, newrecipe) + +def _rename_recipe_files(oldrecipe, bpn, oldpv, newpv, path): + _rename_recipe_dirs(oldpv, newpv, path) + return _rename_recipe_file(oldrecipe, bpn, oldpv, newpv, path) + +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) + + appendpath = os.path.join(workspace, 'appends') + if not os.path.exists(appendpath): + bb.utils.mkdirhier(appendpath) + + brf = os.path.basename(os.path.splitext(rc)[0]) # rc basename + + srctree = os.path.abspath(srctree) + 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') + f.write('inherit externalsrc\n') + f.write(('# NOTE: We use pn- overrides here to avoid affecting' + 'multiple variants in the case where the recipe uses BBCLASSEXTEND\n')) + f.write('EXTERNALSRC_pn-%s = "%s"\n' % (pn, srctree)) + 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('# 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): + rfp = os.path.split(rf)[0] # recipe folder + rfpp = os.path.split(rfp)[0] # recipes folder + if os.path.exists(rfp): + shutil.rmtree(b) + if not len(os.listdir(rfpp)): + os.rmdir(rfpp) + srctree = os.path.abspath(srctree) + if os.path.exists(srctree): + shutil.rmtree(srctree) + +def _upgrade_error(e, rf, srctree): + if rf: + cleanup_on_error(rf, srctree) + logger.error(e) + raise DevtoolError(e) + +def _get_uri(rd): + srcuris = rd.getVar('SRC_URI').split() + if not len(srcuris): + raise DevtoolError('SRC_URI not found on recipe') + # 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 + rev_re = re.compile(';rev=([^;]+)') + res = rev_re.search(srcuri) + if res: + srcrev = res.group(1) + srcuri = rev_re.sub('', srcuri) + return srcuri, srcrev + +def _extract_new_source(newpv, srctree, no_patch, srcrev, srcbranch, branch, keep_temp, tinfoil, rd): + """Extract sources of a recipe with a new version""" + + def __run(cmd): + """Simple wrapper which calls _run with srctree as cwd""" + return _run(cmd, srctree) + + crd = rd.createCopy() + + pv = crd.getVar('PV') + crd.setVar('PV', newpv) + + tmpsrctree = None + uri, rev = _get_uri(crd) + if srcrev: + rev = srcrev + if uri.startswith('git://'): + __run('git fetch') + __run('git checkout %s' % rev) + __run('git tag -f devtool-base-new') + md5 = None + sha256 = None + _, _, _, _, _, params = bb.fetch2.decodeurl(uri) + srcsubdir_rel = params.get('destsuffix', 'git') + if not srcbranch: + check_branch, check_branch_err = __run('git branch -r --contains %s' % srcrev) + get_branch = [x.strip() for x in check_branch.splitlines()] + # Remove HEAD reference point and drop remote prefix + get_branch = [x.split('/', 1)[1] for x in get_branch if not x.startswith('origin/HEAD')] + if 'master' in get_branch: + # If it is master, we do not need to append 'branch=master' as this is default. + # Even with the case where get_branch has multiple objects, if 'master' is one + # of them, we should default take from 'master' + srcbranch = '' + elif len(get_branch) == 1: + # If 'master' isn't in get_branch and get_branch contains only ONE object, then store result into 'srcbranch' + srcbranch = get_branch[0] + else: + # If get_branch contains more than one objects, then display error and exit. + mbrch = '\n ' + '\n '.join(get_branch) + raise DevtoolError('Revision %s was found on multiple branches: %s\nPlease provide the correct branch in the devtool command with "--srcbranch" or "-B" option.' % (srcrev, mbrch)) + else: + __run('git checkout devtool-base -b devtool-%s' % newpv) + + tmpdir = tempfile.mkdtemp(prefix='devtool') + try: + checksums, ftmpdir = scriptutils.fetch_url(tinfoil, uri, rev, tmpdir, logger, preserve_tmp=keep_temp) + except scriptutils.FetchUrlFailure as e: + raise DevtoolError(e) + + if ftmpdir and keep_temp: + logger.info('Fetch temp directory is %s' % ftmpdir) + + md5 = checksums['md5sum'] + sha256 = checksums['sha256sum'] + + tmpsrctree = _get_srctree(tmpdir) + srctree = os.path.abspath(srctree) + srcsubdir_rel = os.path.relpath(tmpsrctree, tmpdir) + + # 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) + + # Copy in new ones + _copy_source_code(tmpsrctree, srctree) + + (stdout,_) = __run('git ls-files --modified --others --exclude-standard') + filelist = stdout.splitlines() + pbar = bb.ui.knotty.BBProgress('Adding changed files', len(filelist)) + pbar.start() + batchsize = 100 + for i in range(0, len(filelist), batchsize): + batch = filelist[i:i+batchsize] + __run('git add -A %s' % ' '.join(['"%s"' % item for item in batch])) + pbar.update(i) + pbar.finish() + + 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') + rev = stdout.rstrip() + + if no_patch: + patches = oe.recipeutils.get_recipe_patches(crd) + if patches: + logger.warn('By user choice, the following patches will NOT be applied to the new source tree:\n %s' % '\n '.join([os.path.basename(patch) for patch in patches])) + else: + __run('git checkout devtool-patched -b %s' % branch) + skiptag = False + try: + __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) + + if tmpsrctree: + if keep_temp: + logger.info('Preserving temporary directory %s' % tmpsrctree) + else: + shutil.rmtree(tmpsrctree) + + return (rev, md5, sha256, srcbranch, srcsubdir_rel) + +def _add_license_diff_to_recipe(path, diff): + notice_text = """# FIXME: the LIC_FILES_CHKSUM values have been updated by 'devtool upgrade'. +# The following is the difference between the old and the new license text. +# Please update the LICENSE value if needed, and summarize the changes in +# the commit message via 'License-Update:' tag. +# (example: 'License-Update: copyright years updated.') +# +# The changes: +# +""" + commented_diff = "\n".join(["# {}".format(l) for l in diff.split('\n')]) + with open(path, 'rb') as f: + orig_content = f.read() + with open(path, 'wb') as f: + f.write(notice_text.encode()) + f.write(commented_diff.encode()) + f.write("\n#\n\n".encode()) + f.write(orig_content) + +def _create_new_recipe(newpv, md5, sha256, srcrev, srcbranch, srcsubdir_old, srcsubdir_new, workspace, tinfoil, rd, license_diff, new_licenses): + """Creates the new recipe under workspace""" + + bpn = rd.getVar('BPN') + path = os.path.join(workspace, 'recipes', bpn) + bb.utils.mkdirhier(path) + copied, _ = oe.recipeutils.copy_recipe_files(rd, path, all_variants=True) + if not copied: + raise DevtoolError('Internal error - no files were copied for recipe %s' % bpn) + logger.debug('Copied %s to %s' % (copied, path)) + + oldpv = rd.getVar('PV') + if not newpv: + newpv = oldpv + 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: + newvalues['PV'] = newpv + + if srcrev: + newvalues['SRCREV'] = srcrev + + if srcbranch: + src_uri = oe.recipeutils.split_var_value(rd.getVar('SRC_URI', False) or '') + changed = False + replacing = True + new_src_uri = [] + for entry in src_uri: + scheme, network, path, user, passwd, params = bb.fetch2.decodeurl(entry) + if replacing and scheme in ['git', 'gitsm']: + branch = params.get('branch', 'master') + if rd.expand(branch) != srcbranch: + # Handle case where branch is set through a variable + res = re.match(r'\$\{([^}@]+)\}', branch) + if res: + newvalues[res.group(1)] = srcbranch + # We know we won't change SRC_URI now, so break out + break + else: + params['branch'] = srcbranch + entry = bb.fetch2.encodeurl((scheme, network, path, user, passwd, params)) + changed = True + replacing = False + new_src_uri.append(entry) + if changed: + newvalues['SRC_URI'] = ' '.join(new_src_uri) + + newvalues['PR'] = None + + # Work out which SRC_URI entries have changed in case the entry uses a name + crd = rd.createCopy() + crd.setVar('PV', newpv) + for var, value in newvalues.items(): + crd.setVar(var, value) + old_src_uri = (rd.getVar('SRC_URI') or '').split() + new_src_uri = (crd.getVar('SRC_URI') or '').split() + newnames = [] + addnames = [] + for newentry in new_src_uri: + _, _, _, _, _, params = bb.fetch2.decodeurl(newentry) + if 'name' in params: + newnames.append(params['name']) + if newentry not in old_src_uri: + addnames.append(params['name']) + # Find what's been set in the original recipe + oldnames = [] + noname = False + for varflag in rd.getVarFlags('SRC_URI'): + if varflag.endswith(('.md5sum', '.sha256sum')): + name = varflag.rsplit('.', 1)[0] + if name not in oldnames: + oldnames.append(name) + elif varflag in ['md5sum', 'sha256sum']: + noname = True + # Even if SRC_URI has named entries it doesn't have to actually use the name + if noname and addnames and addnames[0] not in oldnames: + addnames = [] + # Drop any old names (the name actually might include ${PV}) + for name in oldnames: + if name not in newnames: + newvalues['SRC_URI[%s.md5sum]' % name] = None + newvalues['SRC_URI[%s.sha256sum]' % name] = None + + if md5 and sha256: + if addnames: + nameprefix = '%s.' % addnames[0] + else: + nameprefix = '' + newvalues['SRC_URI[%smd5sum]' % nameprefix] = md5 + newvalues['SRC_URI[%ssha256sum]' % nameprefix] = sha256 + + if srcsubdir_new != srcsubdir_old: + s_subdir_old = os.path.relpath(os.path.abspath(rd.getVar('S')), rd.getVar('WORKDIR')) + s_subdir_new = os.path.relpath(os.path.abspath(crd.getVar('S')), crd.getVar('WORKDIR')) + if srcsubdir_old == s_subdir_old and srcsubdir_new != s_subdir_new: + # Subdir for old extracted source matches what S points to (it should!) + # but subdir for new extracted source doesn't match what S will be + newvalues['S'] = '${WORKDIR}/%s' % srcsubdir_new.replace(newpv, '${PV}') + if crd.expand(newvalues['S']) == crd.expand('${WORKDIR}/${BP}'): + # It's the default, drop it + # FIXME what if S is being set in a .inc? + newvalues['S'] = None + logger.info('Source subdirectory has changed, dropping S value since it now matches the default ("${WORKDIR}/${BP}")') + else: + logger.info('Source subdirectory has changed, updating S value') + + if license_diff: + newlicchksum = " ".join(["file://{};md5={}".format(l["path"], l["actual_md5"]) + (";beginline={}".format(l["beginline"]) if l["beginline"] else "") + (";endline={}".format(l["endline"]) if l["endline"] else "") for l in new_licenses]) + newvalues["LIC_FILES_CHKSUM"] = newlicchksum + _add_license_diff_to_recipe(fullpath, license_diff) + + rd = tinfoil.parse_recipe_file(fullpath, False) + oe.recipeutils.patch_recipe(rd, fullpath, newvalues) + + return fullpath, copied + + +def _check_git_config(): + def getconfig(name): + try: + value = bb.process.run('git config --global %s' % name)[0].strip() + except bb.process.ExecutionError as e: + if e.exitcode == 1: + value = None + else: + raise + return value + + username = getconfig('user.name') + useremail = getconfig('user.email') + configerr = [] + if not username: + configerr.append('Please set your name using:\n git config --global user.name') + if not useremail: + configerr.append('Please set your email using:\n git config --global user.email') + if configerr: + raise DevtoolError('Your git configuration is incomplete which will prevent rebases from working:\n' + '\n'.join(configerr)) + +def _extract_licenses(srcpath, recipe_licenses): + licenses = [] + for url in recipe_licenses.split(): + license = {} + (type, host, path, user, pswd, parm) = bb.fetch.decodeurl(url) + license['path'] = path + license['md5'] = parm.get('md5', '') + license['beginline'], license['endline'] = 0, 0 + if 'beginline' in parm: + license['beginline'] = int(parm['beginline']) + if 'endline' in parm: + license['endline'] = int(parm['endline']) + license['text'] = [] + with open(os.path.join(srcpath, path), 'rb') as f: + import hashlib + actual_md5 = hashlib.md5() + lineno = 0 + for line in f: + lineno += 1 + if (lineno >= license['beginline']) and ((lineno <= license['endline']) or not license['endline']): + license['text'].append(line.decode(errors='ignore')) + actual_md5.update(line) + license['actual_md5'] = actual_md5.hexdigest() + licenses.append(license) + return licenses + +def _generate_license_diff(old_licenses, new_licenses): + need_diff = False + for l in new_licenses: + if l['md5'] != l['actual_md5']: + need_diff = True + break + if need_diff == False: + return None + + import difflib + diff = '' + for old, new in zip(old_licenses, new_licenses): + for line in difflib.unified_diff(old['text'], new['text'], old['path'], new['path']): + diff = diff + line + return diff + +def upgrade(args, config, basepath, workspace): + """Entry point for the devtool 'upgrade' subcommand""" + + if args.recipename in workspace: + raise DevtoolError("recipe %s is already in your workspace" % args.recipename) + 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) + + _check_git_config() + + tinfoil = setup_tinfoil(basepath=basepath, tracking=True) + try: + 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) + + # try to automatically discover latest version and revision if not provided on command line + if not args.version and not args.srcrev: + version_info = oe.recipeutils.get_recipe_upstream_version(rd) + if version_info['version'] and not version_info['version'].endswith("new-commits-available"): + args.version = version_info['version'] + if version_info['revision']: + args.srcrev = version_info['revision'] + if not args.version and not args.srcrev: + raise DevtoolError("Automatic discovery of latest version/revision failed - you must provide a version using the --version/-V option, or for recipes that fetch from an SCM such as git, the --srcrev/-S option.") + + 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") + old_ver = rd.getVar('PV') + if old_ver == args.version and old_srcrev == args.srcrev: + raise DevtoolError("Current and upgrade versions are the same version") + if args.version: + if bb.utils.vercmp_string(args.version, old_ver) < 0: + logger.warning('Upgrade version %s compares as less than the current version %s. If you are using a package feed for on-target upgrades or providing this recipe for general consumption, then you should increment PE in the recipe (or if there is no current PE value set, set it to "1")' % (args.version, old_ver)) + check_prerelease_version(args.version, 'devtool upgrade') + + rf = None + license_diff = None + try: + logger.info('Extracting current version source...') + rev1, srcsubdir1 = standard._extract_source(srctree, False, 'devtool-orig', False, config, basepath, workspace, args.fixed_setup, rd, tinfoil, no_overrides=args.no_overrides) + old_licenses = _extract_licenses(srctree, rd.getVar('LIC_FILES_CHKSUM')) + logger.info('Extracting upgraded version source...') + rev2, md5, sha256, srcbranch, srcsubdir2 = _extract_new_source(args.version, srctree, args.no_patch, + args.srcrev, args.srcbranch, args.branch, args.keep_temp, + tinfoil, rd) + new_licenses = _extract_licenses(srctree, rd.getVar('LIC_FILES_CHKSUM')) + license_diff = _generate_license_diff(old_licenses, new_licenses) + rf, copied = _create_new_recipe(args.version, md5, sha256, args.srcrev, srcbranch, srcsubdir1, srcsubdir2, config.workspace_path, tinfoil, rd, license_diff, new_licenses) + 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) + + update_unlockedsigs(basepath, workspace, args.fixed_setup, [pn]) + + logger.info('Upgraded source extracted to %s' % srctree) + logger.info('New recipe is %s' % rf) + if license_diff: + logger.info('License checksums have been updated in the new recipe; please refer to it for the difference between the old and the new license texts.') + finally: + tinfoil.shutdown() + return 0 + +def latest_version(args, config, basepath, workspace): + """Entry point for the devtool 'latest_version' subcommand""" + tinfoil = setup_tinfoil(basepath=basepath, tracking=True) + try: + rd = parse_recipe(config, tinfoil, args.recipename, True) + if not rd: + return 1 + version_info = oe.recipeutils.get_recipe_upstream_version(rd) + # "new-commits-available" is an indication that upstream never issues version tags + if not version_info['version'].endswith("new-commits-available"): + logger.info("Current version: {}".format(version_info['current_version'])) + logger.info("Latest version: {}".format(version_info['version'])) + if version_info['revision']: + logger.info("Latest version's commit: {}".format(version_info['revision'])) + else: + logger.info("Latest commit: {}".format(version_info['revision'])) + 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).', + 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', 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). If omitted, latest upstream version will be determined and used, if possible.') + parser_upgrade.add_argument('--srcrev', '-S', help='Source revision to upgrade to (useful when 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') + parser_upgrade.add_argument('--no-overrides', '-O', action="store_true", help='Do not create branches for other override configurations') + group = parser_upgrade.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_upgrade.add_argument('--keep-temp', action="store_true", help='Keep temporary directory (for debugging)') + parser_upgrade.set_defaults(func=upgrade, fixed_setup=context.fixed_setup) + + parser_latest_version = subparsers.add_parser('latest-version', help='Report the latest version of an existing recipe', + description='Queries the upstream server for what the latest upstream release is (for git, tags are checked, for tarballs, a list of them is obtained, and one with the highest version number is reported)', + group='info') + parser_latest_version.add_argument('recipename', help='Name of recipe to query (just name - no version, path or extension)') + parser_latest_version.set_defaults(func=latest_version) diff --git a/poky/scripts/lib/devtool/utilcmds.py b/poky/scripts/lib/devtool/utilcmds.py new file mode 100644 index 000000000..7cd139fb8 --- /dev/null +++ b/poky/scripts/lib/devtool/utilcmds.py @@ -0,0 +1,252 @@ +# Development tool - utility commands 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. + +"""Devtool utility plugins""" + +import os +import sys +import shutil +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 + +logger = logging.getLogger('devtool') + +def _find_recipe_path(args, config, basepath, workspace): + if args.any_recipe: + logger.warning('-a/--any-recipe option is now always active, and thus the option will be removed in a future release') + if args.recipename in workspace: + recipefile = workspace[args.recipename]['recipefile'] + else: + recipefile = None + if not recipefile: + tinfoil = setup_tinfoil(config_only=False, basepath=basepath) + try: + rd = parse_recipe(config, tinfoil, args.recipename, True) + if not rd: + raise DevtoolError("Failed to find specified recipe") + recipefile = rd.getVar('FILE') + finally: + tinfoil.shutdown() + return recipefile + + +def find_recipe(args, config, basepath, workspace): + """Entry point for the devtool 'find-recipe' subcommand""" + recipefile = _find_recipe_path(args, config, basepath, workspace) + print(recipefile) + return 0 + + +def edit_recipe(args, config, basepath, workspace): + """Entry point for the devtool 'edit-recipe' subcommand""" + return scriptutils.run_editor(_find_recipe_path(args, config, basepath, workspace), logger) + + +def configure_help(args, config, basepath, workspace): + """Entry point for the devtool 'configure-help' subcommand""" + import oe.utils + + check_workspace_recipe(workspace, args.recipename) + tinfoil = setup_tinfoil(config_only=False, basepath=basepath) + try: + rd = parse_recipe(config, tinfoil, args.recipename, appends=True, filter_workspace=False) + if not rd: + return 1 + 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') 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') + pn = rd.getVar('PN') + finally: + tinfoil.shutdown() + + if 'doc' in packageconfig: + del packageconfig['doc'] + + if autotools and not os.path.exists(configurescript): + logger.info('Running do_configure to generate configure script') + try: + stdout, _ = exec_build_env_command(config.init_path, basepath, + 'bitbake -c configure %s' % args.recipename, + stderr=subprocess.STDOUT) + except bb.process.ExecutionError: + pass + + if confdisabled or do_configure.strip() in ('', ':'): + raise DevtoolError("do_configure task has been disabled for this recipe") + elif args.no_pager and not os.path.exists(configurescript): + raise DevtoolError("No configure script found and no other information to display") + else: + configopttext = '' + if autotools and configureopts: + configopttext = ''' +Arguments currently passed to the configure script: + +%s + +Some of those are fixed.''' % (configureopts + ' ' + extra_oeconf) + if extra_oeconf: + configopttext += ''' The ones that are specified through EXTRA_OECONF (which you can change or add to easily): + +%s''' % extra_oeconf + + elif cmake: + in_cmake = False + cmake_cmd = '' + for line in cmake_do_configure.splitlines(): + if in_cmake: + cmake_cmd = cmake_cmd + ' ' + line.strip().rstrip('\\') + if not line.endswith('\\'): + break + if line.lstrip().startswith('cmake '): + cmake_cmd = line.strip().rstrip('\\') + if line.endswith('\\'): + in_cmake = True + else: + break + if cmake_cmd: + configopttext = ''' +The current cmake command line: + +%s + +Arguments specified through EXTRA_OECMAKE (which you can change or add to easily) + +%s''' % (oe.utils.squashspaces(cmake_cmd), extra_oecmake) + else: + configopttext = ''' +The current implementation of cmake_do_configure: + +cmake_do_configure() { +%s +} + +Arguments specified through EXTRA_OECMAKE (which you can change or add to easily) + +%s''' % (cmake_do_configure.rstrip(), extra_oecmake) + + elif do_configure: + configopttext = ''' +The current implementation of do_configure: + +do_configure() { +%s +}''' % do_configure.rstrip() + if '${EXTRA_OECONF}' in do_configure_noexpand: + configopttext += ''' + +Arguments specified through EXTRA_OECONF (which you can change or add to easily): + +%s''' % extra_oeconf + + if packageconfig: + configopttext += ''' + +Some of these options may be controlled through PACKAGECONFIG; for more details please see the recipe.''' + + if args.arg: + helpargs = ' '.join(args.arg) + elif cmake: + helpargs = '-LH' + else: + helpargs = '--help' + + msg = '''configure information for %s +------------------------------------------ +%s''' % (pn, configopttext) + + if cmake: + msg += ''' + +The cmake %s output for %s follows. After "-- Cache values" you should see a list of variables you can add to EXTRA_OECMAKE (prefixed with -D and suffixed with = followed by the desired value, without any spaces). +------------------------------------------''' % (helpargs, pn) + elif os.path.exists(configurescript): + msg += ''' + +The ./configure %s output for %s follows. +------------------------------------------''' % (helpargs, pn) + + olddir = os.getcwd() + tmppath = tempfile.mkdtemp() + with tempfile.NamedTemporaryFile('w', delete=False) as tf: + if not args.no_header: + tf.write(msg + '\n') + tf.close() + try: + try: + cmd = 'cat %s' % tf.name + if cmake: + cmd += '; cmake %s %s 2>&1' % (helpargs, s) + os.chdir(b) + elif os.path.exists(configurescript): + cmd += '; %s %s' % (configurescript, helpargs) + if sys.stdout.isatty() and not args.no_pager: + pager = os.environ.get('PAGER', 'less') + cmd = '(%s) | %s' % (cmd, pager) + subprocess.check_call(cmd, shell=True) + except subprocess.CalledProcessError as e: + return e.returncode + finally: + os.chdir(olddir) + shutil.rmtree(tmppath) + os.remove(tf.name) + + +def register_commands(subparsers, context): + """Register devtool subcommands from this plugin""" + parser_edit_recipe = subparsers.add_parser('edit-recipe', help='Edit a recipe file', + description='Runs the default editor (as specified by the EDITOR variable) on the specified recipe. Note that this will be quicker for recipes in the workspace as the cache does not need to be loaded in that case.', + group='working') + parser_edit_recipe.add_argument('recipename', help='Recipe to edit') + # FIXME drop -a at some point in future + parser_edit_recipe.add_argument('--any-recipe', '-a', action="store_true", help='Does nothing (exists for backwards-compatibility)') + parser_edit_recipe.set_defaults(func=edit_recipe) + + # Find-recipe + parser_find_recipe = subparsers.add_parser('find-recipe', help='Find a recipe file', + description='Finds a recipe file. Note that this will be quicker for recipes in the workspace as the cache does not need to be loaded in that case.', + group='working') + parser_find_recipe.add_argument('recipename', help='Recipe to find') + # FIXME drop -a at some point in future + parser_find_recipe.add_argument('--any-recipe', '-a', action="store_true", help='Does nothing (exists for backwards-compatibility)') + parser_find_recipe.set_defaults(func=find_recipe) + + # NOTE: Needed to override the usage string here since the default + # 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.', + 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") + parser_configure_help.add_argument('--arg', help='Pass remaining arguments to the configure script instead of --help (useful if the script has additional help options)', nargs=argparse.REMAINDER) + parser_configure_help.set_defaults(func=configure_help) |