summaryrefslogtreecommitdiff
path: root/scripts/lib/devtool/standard.py
diff options
context:
space:
mode:
Diffstat (limited to 'scripts/lib/devtool/standard.py')
-rw-r--r--scripts/lib/devtool/standard.py1918
1 files changed, 1918 insertions, 0 deletions
diff --git a/scripts/lib/devtool/standard.py b/scripts/lib/devtool/standard.py
new file mode 100644
index 0000000000..5ff1e230fd
--- /dev/null
+++ b/scripts/lib/devtool/standard.py
@@ -0,0 +1,1918 @@
+# Development tool - standard commands 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 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, ensure_npm, DevtoolError
+from devtool import parse_recipe
+
+logger = logging.getLogger('devtool')
+
+
+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 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:
+ if args.fetchuri.startswith('npm://'):
+ ensure_npm(config, basepath, args.fixed_setup)
+
+ 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'
+
+ tempdir = tempfile.mkdtemp(prefix='devtool')
+ try:
+ builtnpm = False
+ while True:
+ try:
+ stdout, _ = exec_build_env_command(config.init_path, basepath, 'recipetool --color=%s create --devtool -o %s \'%s\' %s' % (color, tempdir, source, extracmdopts), watch=True)
+ except bb.process.ExecutionError as e:
+ if e.exitcode == 14:
+ if builtnpm:
+ raise DevtoolError('Re-running recipetool still failed to find npm')
+ # FIXME this is a horrible hack that is unfortunately
+ # necessary due to the fact that we can't run bitbake from
+ # inside recipetool since recipetool keeps tinfoil active
+ # with references to it throughout the code, so we have
+ # to exit out and come back here to do it.
+ ensure_npm(config, basepath, args.fixed_setup, check_exists=False)
+ logger.info('Re-running recipe creation process after building nodejs')
+ builtnpm = True
+ continue
+ elif e.exitcode == 15:
+ raise DevtoolError('Could not auto-determine recipe name, please specify it on the command line')
+ else:
+ raise DevtoolError('Command \'%s\' failed' % e.command)
+ break
+
+ recipes = glob.glob(os.path.join(tempdir, '*.bb'))
+ if recipes:
+ 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')
+
+ _add_md5(config, recipename, appendfile)
+
+ 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 _move_file(src, dst):
+ """Move a file. Creates all the directory components of destination path."""
+ dst_d = os.path.dirname(dst)
+ if dst_d:
+ bb.utils.mkdirhier(dst_d)
+ shutil.move(src, dst)
+
+def _copy_file(src, dst):
+ """Copy a file. Creates all the directory components of destination path."""
+ dst_d = os.path.dirname(dst)
+ if dst_d:
+ bb.utils.mkdirhier(dst_d)
+ shutil.copy(src, dst)
+
+def _git_ls_tree(repodir, treeish='HEAD', recursive=False):
+ """List contents of a git treeish"""
+ import bb
+ 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 = _prep_extract_operation(config, basepath, args.recipename)
+ 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, rd, tinfoil)
+ 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 = _prep_extract_operation(config, basepath, args.recipename)
+ 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, rd, tinfoil)
+ logger.info('Source tree %s synchronized' % srctree)
+
+ if initial_rev:
+ return 0
+ else:
+ return 1
+ finally:
+ tinfoil.shutdown()
+
+
+def _prep_extract_operation(config, basepath, recipename, tinfoil=None):
+ """HACK: Ugly workaround for making sure that requirements are met when
+ trying to extract a package. Returns the tinfoil instance to be used."""
+ if not tinfoil:
+ tinfoil = setup_tinfoil(basepath=basepath)
+
+ rd = parse_recipe(config, tinfoil, recipename, True)
+ if not rd:
+ return None
+
+ if bb.data.inherits_class('kernel-yocto', rd):
+ tinfoil.shutdown()
+ try:
+ stdout, _ = exec_build_env_command(config.init_path, basepath,
+ 'bitbake kern-tools-native')
+ tinfoil = setup_tinfoil(basepath=basepath)
+ except bb.process.ExecutionError as err:
+ raise DevtoolError("Failed to build kern-tools-native:\n%s" %
+ err.stdout)
+ return tinfoil
+
+
+def _extract_source(srctree, keep_temp, devbranch, sync, d, tinfoil):
+ """Extract sources of a recipe"""
+ import oe.recipeutils
+
+ 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)
+
+ initial_rev = None
+ # We need to redirect WORKDIR, STAMPS_DIR etc. under a temporary
+ # directory so that:
+ # (a) we pick up all files that get unpacked to the WORKDIR, and
+ # (b) we don't disturb the existing build
+ # However, with recipe-specific sysroots the sysroots for the recipe
+ # will be prepared under WORKDIR, and if we used the system temporary
+ # directory (i.e. usually /tmp) as used by mkdtemp by default, then
+ # our attempts to hardlink files into the recipe-specific sysroots
+ # will fail on systems where /tmp is a different filesystem, and it
+ # would have to fall back to copying the files which is a waste of
+ # time. Put the temp directory under the WORKDIR to prevent that from
+ # being a problem.
+ tempbasedir = d.getVar('WORKDIR')
+ bb.utils.mkdirhier(tempbasedir)
+ tempdir = tempfile.mkdtemp(prefix='devtooltmp-', dir=tempbasedir)
+ try:
+ tinfoil.logger.setLevel(logging.WARNING)
+
+ crd = d.createCopy()
+ # Make a subdir so we guard against WORKDIR==S
+ workdir = os.path.join(tempdir, 'workdir')
+ crd.setVar('WORKDIR', workdir)
+ if not crd.getVar('S').startswith(workdir):
+ # Usually a shared workdir recipe (kernel, gcc)
+ # Try to set a reasonable default
+ if bb.data.inherits_class('kernel', d):
+ crd.setVar('S', '${WORKDIR}/source')
+ else:
+ crd.setVar('S', '${WORKDIR}/%s' % os.path.basename(d.getVar('S')))
+ if bb.data.inherits_class('kernel', d):
+ # We don't want to move the source to STAGING_KERNEL_DIR here
+ crd.setVar('STAGING_KERNEL_DIR', '${S}')
+
+ is_kernel_yocto = bb.data.inherits_class('kernel-yocto', d)
+ if not is_kernel_yocto:
+ crd.setVar('PATCHTOOL', 'git')
+ crd.setVar('PATCH_COMMIT_FUNCTIONS', '1')
+
+ # Apply our changes to the datastore to the server's datastore
+ for key in crd.localkeys():
+ tinfoil.config_data.setVar('%s_pn-%s' % (key, pn), crd.getVar(key, False))
+
+ tinfoil.config_data.setVar('STAMPS_DIR', os.path.join(tempdir, 'stamps'))
+ tinfoil.config_data.setVar('T', os.path.join(tempdir, 'temp'))
+ tinfoil.config_data.setVar('BUILDCFG_FUNCS', '')
+ tinfoil.config_data.setVar('BUILDCFG_HEADER', '')
+ tinfoil.config_data.setVar('BB_HASH_IGNORE_MISMATCH', '1')
+
+ tinfoil.set_event_mask(['bb.event.BuildStarted',
+ 'bb.event.BuildCompleted',
+ 'logging.LogRecord',
+ 'bb.command.CommandCompleted',
+ 'bb.command.CommandFailed',
+ 'bb.build.TaskStarted',
+ 'bb.build.TaskSucceeded',
+ 'bb.build.TaskFailed',
+ 'bb.build.TaskFailedSilent'])
+
+ def runtask(target, task):
+ if tinfoil.build_file(target, task):
+ while True:
+ event = tinfoil.wait_event(0.25)
+ if event:
+ if isinstance(event, bb.command.CommandCompleted):
+ break
+ elif isinstance(event, bb.command.CommandFailed):
+ raise DevtoolError('Task do_%s failed: %s' % (task, event.error))
+ elif isinstance(event, bb.build.TaskFailed):
+ raise DevtoolError('Task do_%s failed' % task)
+ elif isinstance(event, bb.build.TaskStarted):
+ logger.info('Executing %s...' % event._task)
+ elif isinstance(event, logging.LogRecord):
+ if event.levelno <= logging.INFO:
+ continue
+ logger.handle(event)
+
+ # we need virtual:native:/path/to/recipe if it's a BBCLASSEXTEND
+ fn = tinfoil.get_recipe_file(pn)
+ runtask(fn, 'unpack')
+
+ if bb.data.inherits_class('kernel-yocto', d):
+ # Extra step for kernel to populate the source directory
+ runtask(fn, 'kernel_checkout')
+
+ srcsubdir = crd.getVar('S')
+
+ # Move local source files into separate subdir
+ recipe_patches = [os.path.basename(patch) for patch in
+ oe.recipeutils.get_recipe_patches(crd)]
+ local_files = oe.recipeutils.get_recipe_local_files(crd)
+
+ # Ignore local files with subdir={BP}
+ srcabspath = os.path.abspath(srcsubdir)
+ local_files = [fname for fname in local_files if
+ os.path.exists(os.path.join(workdir, fname)) and
+ (srcabspath == workdir or not
+ os.path.join(workdir, fname).startswith(srcabspath +
+ os.sep))]
+ if local_files:
+ for fname in local_files:
+ _move_file(os.path.join(workdir, fname),
+ os.path.join(tempdir, 'oe-local-files', fname))
+ with open(os.path.join(tempdir, 'oe-local-files', '.gitignore'),
+ 'w') as f:
+ f.write('# Ignore local files, by default. Remove this file '
+ 'if you want to commit the directory to Git\n*\n')
+
+ if srcsubdir == workdir:
+ # Find non-patch non-local sources that were "unpacked" to srctree
+ # directory
+ src_files = [fname for fname in _ls_tree(workdir) if
+ os.path.basename(fname) not in recipe_patches]
+ # Force separate S so that patch files can be left out from srctree
+ srcsubdir = tempfile.mkdtemp(dir=workdir)
+ tinfoil.config_data.setVar('S_task-patch', srcsubdir)
+ # Move source files to S
+ for path in src_files:
+ _move_file(os.path.join(workdir, path),
+ os.path.join(srcsubdir, path))
+ elif os.path.dirname(srcsubdir) != workdir:
+ # Handle if S is set to a subdirectory of the source
+ srcsubdir = os.path.join(workdir, os.path.relpath(srcsubdir, workdir).split(os.sep)[0])
+
+ scriptutils.git_convert_standalone_clone(srcsubdir)
+
+ # Make sure that srcsubdir exists
+ bb.utils.mkdirhier(srcsubdir)
+ if not os.path.exists(srcsubdir) or not os.listdir(srcsubdir):
+ logger.warning("no source unpacked to S, either the %s recipe "
+ "doesn't use any source or the correct source "
+ "directory could not be determined" % pn)
+
+ setup_git_repo(srcsubdir, crd.getVar('PV'), devbranch, d=d)
+
+ (stdout, _) = bb.process.run('git rev-parse HEAD', cwd=srcsubdir)
+ initial_rev = stdout.rstrip()
+
+ logger.info('Patching...')
+ runtask(fn, 'patch')
+
+ bb.process.run('git tag -f devtool-patched', cwd=srcsubdir)
+
+ kconfig = None
+ if bb.data.inherits_class('kernel-yocto', d):
+ # Store generate and store kernel config
+ logger.info('Generating kernel config')
+ runtask(fn, 'configure')
+ kconfig = os.path.join(crd.getVar('B'), '.config')
+
+
+ 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 kconfig:
+ logger.info('Copying kernel config to srctree')
+ shutil.copy2(kconfig, srctree)
+
+ finally:
+ if keep_temp:
+ logger.info('Preserving temporary directory %s' % tempdir)
+ else:
+ shutil.rmtree(tempdir)
+ return initial_rev
+
+def _add_md5(config, recipename, filename):
+ """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:
+ f.write('%s|%s|%s\n' % (recipename, os.path.relpath(fn, config.workspace_path), md5))
+
+ 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
+
+ if args.recipename in workspace:
+ raise DevtoolError("recipe %s is already in your workspace" %
+ args.recipename)
+
+ tinfoil = setup_tinfoil(basepath=basepath)
+ 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)
+ if not args.no_extract:
+ tinfoil = _prep_extract_operation(config, basepath, pn, tinfoil)
+ if not tinfoil:
+ # Error already shown
+ return 1
+ # We need to re-parse because tinfoil may have been re-initialised
+ rd = parse_recipe(config, tinfoil, args.recipename, True)
+
+ recipefile = rd.getVar('FILE')
+ appendfile = recipe_to_append(recipefile, config, args.wildcard)
+ if os.path.exists(appendfile):
+ raise DevtoolError("Another variant of recipe %s is already in your "
+ "workspace (only one variant of a recipe can "
+ "currently be worked on at once)"
+ % pn)
+
+ _check_compatible_recipe(pn, rd)
+
+ initial_rev = None
+ commits = []
+ if not args.no_extract:
+ initial_rev = _extract_source(srctree, args.keep_temp, args.branch, False, rd, tinfoil)
+ if not initial_rev:
+ return 1
+ logger.info('Source tree extracted to %s' % srctree)
+ # Get list of commits since this revision
+ (stdout, _) = bb.process.run('git rev-list --reverse %s..HEAD' % initial_rev, cwd=srctree)
+ commits = stdout.split()
+ else:
+ if os.path.exists(os.path.join(srctree, '.git')):
+ # Check if it's a tree previously extracted by us
+ try:
+ (stdout, _) = bb.process.run('git branch --contains devtool-base', cwd=srctree)
+ except bb.process.ExecutionError:
+ stdout = ''
+ for line in stdout.splitlines():
+ if line.startswith('*'):
+ (stdout, _) = bb.process.run('git rev-parse devtool-base', cwd=srctree)
+ initial_rev = stdout.rstrip()
+ if not initial_rev:
+ # Otherwise, just grab the head revision
+ (stdout, _) = bb.process.run('git rev-parse HEAD', cwd=srctree)
+ initial_rev = stdout.rstrip()
+
+ # Check that recipe isn't using a shared workdir
+ s = os.path.abspath(rd.getVar('S'))
+ workdir = os.path.abspath(rd.getVar('WORKDIR'))
+ if s.startswith(workdir) and s != workdir and os.path.dirname(s) != workdir:
+ # Handle if S is set to a subdirectory of the source
+ srcsubdir = os.path.relpath(s, workdir).split(os.sep, 1)[1]
+ srctree = os.path.join(srctree, srcsubdir)
+
+ bb.utils.mkdirhier(os.path.dirname(appendfile))
+ with open(appendfile, 'w') as f:
+ f.write('FILESEXTRAPATHS_prepend := "${THISDIR}/${PN}:"\n')
+ # Local files can be modified/tracked in separate subdir under srctree
+ # Mostly useful for packages with S != WORKDIR
+ f.write('FILESPATH_prepend := "%s:"\n' %
+ os.path.join(srctree, 'oe-local-files'))
+
+ f.write('\ninherit externalsrc\n')
+ f.write('# NOTE: We use pn- overrides here to avoid affecting multiple variants in the case where the recipe uses BBCLASSEXTEND\n')
+ f.write('EXTERNALSRC_pn-%s = "%s"\n' % (pn, srctree))
+
+ b_is_s = use_external_build(args.same_dir, args.no_same_dir, rd)
+ if b_is_s:
+ f.write('EXTERNALSRC_BUILD_pn-%s = "%s"\n' % (pn, srctree))
+
+ if bb.data.inherits_class('kernel', rd):
+ f.write('SRCTREECOVEREDTASKS = "do_validate_branches do_kernel_checkout '
+ 'do_fetch do_unpack do_patch do_kernel_configme do_kernel_configcheck"\n')
+ f.write('\ndo_configure_append() {\n'
+ ' cp ${B}/.config ${S}/.config.baseline\n'
+ ' ln -sfT ${B}/.config ${S}/.config.new\n'
+ '}\n')
+ if initial_rev:
+ f.write('\n# initial_rev: %s\n' % initial_rev)
+ for commit in commits:
+ f.write('# commit: %s\n' % commit)
+
+ _add_md5(config, pn, appendfile)
+
+ logger.info('Recipe %s now set up to build from %s' % (pn, srctree))
+
+ 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
+
+ appen