1# Development tool - standard commands plugin
2#
3# Copyright (C) 2014-2017 Intel Corporation
4#
5# SPDX-License-Identifier: GPL-2.0-only
6#
7"""Devtool standard plugins"""
8
9import os
10import sys
11import re
12import shutil
13import subprocess
14import tempfile
15import logging
16import argparse
17import argparse_oe
18import scriptutils
19import errno
20import glob
21import filecmp
22from collections import OrderedDict
23from 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
24from devtool import parse_recipe
25
26logger = logging.getLogger('devtool')
27
28override_branch_prefix = 'devtool-override-'
29
30
31def add(args, config, basepath, workspace):
32    """Entry point for the devtool 'add' subcommand"""
33    import bb
34    import oe.recipeutils
35
36    if not args.recipename and not args.srctree and not args.fetch and not args.fetchuri:
37        raise argparse_oe.ArgumentUsageError('At least one of recipename, srctree, fetchuri or -f/--fetch must be specified', 'add')
38
39    # These are positional arguments, but because we're nice, allow
40    # specifying e.g. source tree without name, or fetch URI without name or
41    # source tree (if we can detect that that is what the user meant)
42    if scriptutils.is_src_url(args.recipename):
43        if not args.fetchuri:
44            if args.fetch:
45                raise DevtoolError('URI specified as positional argument as well as -f/--fetch')
46            args.fetchuri = args.recipename
47            args.recipename = ''
48    elif scriptutils.is_src_url(args.srctree):
49        if not args.fetchuri:
50            if args.fetch:
51                raise DevtoolError('URI specified as positional argument as well as -f/--fetch')
52            args.fetchuri = args.srctree
53            args.srctree = ''
54    elif args.recipename and not args.srctree:
55        if os.sep in args.recipename:
56            args.srctree = args.recipename
57            args.recipename = None
58        elif os.path.isdir(args.recipename):
59            logger.warning('Ambiguous argument "%s" - assuming you mean it to be the recipe name' % args.recipename)
60
61    if not args.fetchuri:
62        if args.srcrev:
63            raise DevtoolError('The -S/--srcrev option is only valid when fetching from an SCM repository')
64        if args.srcbranch:
65            raise DevtoolError('The -B/--srcbranch option is only valid when fetching from an SCM repository')
66
67    if args.srctree and os.path.isfile(args.srctree):
68        args.fetchuri = 'file://' + os.path.abspath(args.srctree)
69        args.srctree = ''
70
71    if args.fetch:
72        if args.fetchuri:
73            raise DevtoolError('URI specified as positional argument as well as -f/--fetch')
74        else:
75            logger.warning('-f/--fetch option is deprecated - you can now simply specify the URL to fetch as a positional argument instead')
76            args.fetchuri = args.fetch
77
78    if args.recipename:
79        if args.recipename in workspace:
80            raise DevtoolError("recipe %s is already in your workspace" %
81                               args.recipename)
82        reason = oe.recipeutils.validate_pn(args.recipename)
83        if reason:
84            raise DevtoolError(reason)
85
86    if args.srctree:
87        srctree = os.path.abspath(args.srctree)
88        srctreeparent = None
89        tmpsrcdir = None
90    else:
91        srctree = None
92        srctreeparent = get_default_srctree(config)
93        bb.utils.mkdirhier(srctreeparent)
94        tmpsrcdir = tempfile.mkdtemp(prefix='devtoolsrc', dir=srctreeparent)
95
96    if srctree and os.path.exists(srctree):
97        if args.fetchuri:
98            if not os.path.isdir(srctree):
99                raise DevtoolError("Cannot fetch into source tree path %s as "
100                                   "it exists and is not a directory" %
101                                   srctree)
102            elif os.listdir(srctree):
103                raise DevtoolError("Cannot fetch into source tree path %s as "
104                                   "it already exists and is non-empty" %
105                                   srctree)
106    elif not args.fetchuri:
107        if args.srctree:
108            raise DevtoolError("Specified source tree %s could not be found" %
109                               args.srctree)
110        elif srctree:
111            raise DevtoolError("No source tree exists at default path %s - "
112                               "either create and populate this directory, "
113                               "or specify a path to a source tree, or a "
114                               "URI to fetch source from" % srctree)
115        else:
116            raise DevtoolError("You must either specify a source tree "
117                               "or a URI to fetch source from")
118
119    if args.version:
120        if '_' in args.version or ' ' in args.version:
121            raise DevtoolError('Invalid version string "%s"' % args.version)
122
123    if args.color == 'auto' and sys.stdout.isatty():
124        color = 'always'
125    else:
126        color = args.color
127    extracmdopts = ''
128    if args.fetchuri:
129        source = args.fetchuri
130        if srctree:
131            extracmdopts += ' -x %s' % srctree
132        else:
133            extracmdopts += ' -x %s' % tmpsrcdir
134    else:
135        source = srctree
136    if args.recipename:
137        extracmdopts += ' -N %s' % args.recipename
138    if args.version:
139        extracmdopts += ' -V %s' % args.version
140    if args.binary:
141        extracmdopts += ' -b'
142    if args.also_native:
143        extracmdopts += ' --also-native'
144    if args.src_subdir:
145        extracmdopts += ' --src-subdir "%s"' % args.src_subdir
146    if args.autorev:
147        extracmdopts += ' -a'
148    if args.npm_dev:
149        extracmdopts += ' --npm-dev'
150    if args.mirrors:
151        extracmdopts += ' --mirrors'
152    if args.srcrev:
153        extracmdopts += ' --srcrev %s' % args.srcrev
154    if args.srcbranch:
155        extracmdopts += ' --srcbranch %s' % args.srcbranch
156    if args.provides:
157        extracmdopts += ' --provides %s' % args.provides
158
159    tempdir = tempfile.mkdtemp(prefix='devtool')
160    try:
161        try:
162            stdout, _ = exec_build_env_command(config.init_path, basepath, 'recipetool --color=%s create --devtool -o %s \'%s\' %s' % (color, tempdir, source, extracmdopts), watch=True)
163        except bb.process.ExecutionError as e:
164            if e.exitcode == 15:
165                raise DevtoolError('Could not auto-determine recipe name, please specify it on the command line')
166            else:
167                raise DevtoolError('Command \'%s\' failed' % e.command)
168
169        recipes = glob.glob(os.path.join(tempdir, '*.bb'))
170        if recipes:
171            recipename = os.path.splitext(os.path.basename(recipes[0]))[0].split('_')[0]
172            if recipename in workspace:
173                raise DevtoolError('A recipe with the same name as the one being created (%s) already exists in your workspace' % recipename)
174            recipedir = os.path.join(config.workspace_path, 'recipes', recipename)
175            bb.utils.mkdirhier(recipedir)
176            recipefile = os.path.join(recipedir, os.path.basename(recipes[0]))
177            appendfile = recipe_to_append(recipefile, config)
178            if os.path.exists(appendfile):
179                # This shouldn't be possible, but just in case
180                raise DevtoolError('A recipe with the same name as the one being created already exists in your workspace')
181            if os.path.exists(recipefile):
182                raise DevtoolError('A recipe file %s already exists in your workspace; this shouldn\'t be there - please delete it before continuing' % recipefile)
183            if tmpsrcdir:
184                srctree = os.path.join(srctreeparent, recipename)
185                if os.path.exists(tmpsrcdir):
186                    if os.path.exists(srctree):
187                        if os.path.isdir(srctree):
188                            try:
189                                os.rmdir(srctree)
190                            except OSError as e:
191                                if e.errno == errno.ENOTEMPTY:
192                                    raise DevtoolError('Source tree path %s already exists and is not empty' % srctree)
193                                else:
194                                    raise
195                        else:
196                            raise DevtoolError('Source tree path %s already exists and is not a directory' % srctree)
197                    logger.info('Using default source tree path %s' % srctree)
198                    shutil.move(tmpsrcdir, srctree)
199                else:
200                    raise DevtoolError('Couldn\'t find source tree created by recipetool')
201            bb.utils.mkdirhier(recipedir)
202            shutil.move(recipes[0], recipefile)
203            # Move any additional files created by recipetool
204            for fn in os.listdir(tempdir):
205                shutil.move(os.path.join(tempdir, fn), recipedir)
206        else:
207            raise DevtoolError('Command \'%s\' did not create any recipe file:\n%s' % (e.command, e.stdout))
208        attic_recipe = os.path.join(config.workspace_path, 'attic', recipename, os.path.basename(recipefile))
209        if os.path.exists(attic_recipe):
210            logger.warning('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)
211    finally:
212        if tmpsrcdir and os.path.exists(tmpsrcdir):
213            shutil.rmtree(tmpsrcdir)
214        shutil.rmtree(tempdir)
215
216    for fn in os.listdir(recipedir):
217        _add_md5(config, recipename, os.path.join(recipedir, fn))
218
219    tinfoil = setup_tinfoil(config_only=True, basepath=basepath)
220    try:
221        try:
222            rd = tinfoil.parse_recipe_file(recipefile, False)
223        except Exception as e:
224            logger.error(str(e))
225            rd = None
226        if not rd:
227            # Parsing failed. We just created this recipe and we shouldn't
228            # leave it in the workdir or it'll prevent bitbake from starting
229            movefn = '%s.parsefailed' % recipefile
230            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)
231            shutil.move(recipefile, movefn)
232            return 1
233
234        if args.fetchuri and not args.no_git:
235            setup_git_repo(srctree, args.version, 'devtool', d=tinfoil.config_data)
236
237        initial_rev = None
238        if os.path.exists(os.path.join(srctree, '.git')):
239            (stdout, _) = bb.process.run('git rev-parse HEAD', cwd=srctree)
240            initial_rev = stdout.rstrip()
241
242        if args.src_subdir:
243            srctree = os.path.join(srctree, args.src_subdir)
244
245        bb.utils.mkdirhier(os.path.dirname(appendfile))
246        with open(appendfile, 'w') as f:
247            f.write('inherit externalsrc\n')
248            f.write('EXTERNALSRC = "%s"\n' % srctree)
249
250            b_is_s = use_external_build(args.same_dir, args.no_same_dir, rd)
251            if b_is_s:
252                f.write('EXTERNALSRC_BUILD = "%s"\n' % srctree)
253            if initial_rev:
254                f.write('\n# initial_rev: %s\n' % initial_rev)
255
256            if args.binary:
257                f.write('do_install:append() {\n')
258                f.write('    rm -rf ${D}/.git\n')
259                f.write('    rm -f ${D}/singletask.lock\n')
260                f.write('}\n')
261
262            if bb.data.inherits_class('npm', rd):
263                f.write('python do_configure:append() {\n')
264                f.write('    pkgdir = d.getVar("NPM_PACKAGE")\n')
265                f.write('    lockfile = os.path.join(pkgdir, "singletask.lock")\n')
266                f.write('    bb.utils.remove(lockfile)\n')
267                f.write('}\n')
268
269        # Check if the new layer provides recipes whose priorities have been
270        # overriden by PREFERRED_PROVIDER.
271        recipe_name = rd.getVar('PN')
272        provides = rd.getVar('PROVIDES')
273        # Search every item defined in PROVIDES
274        for recipe_provided in provides.split():
275            preferred_provider = 'PREFERRED_PROVIDER_' + recipe_provided
276            current_pprovider = rd.getVar(preferred_provider)
277            if current_pprovider and current_pprovider != recipe_name:
278                if args.fixed_setup:
279                    #if we are inside the eSDK add the new PREFERRED_PROVIDER in the workspace layer.conf
280                    layerconf_file = os.path.join(config.workspace_path, "conf", "layer.conf")
281                    with open(layerconf_file, 'a') as f:
282                        f.write('%s = "%s"\n' % (preferred_provider, recipe_name))
283                else:
284                    logger.warning('Set \'%s\' in order to use the recipe' % preferred_provider)
285                break
286
287        _add_md5(config, recipename, appendfile)
288
289        check_prerelease_version(rd.getVar('PV'), 'devtool add')
290
291        logger.info('Recipe %s has been automatically created; further editing may be required to make it fully functional' % recipefile)
292
293    finally:
294        tinfoil.shutdown()
295
296    return 0
297
298
299def _check_compatible_recipe(pn, d):
300    """Check if the recipe is supported by devtool"""
301    if pn == 'perf':
302        raise DevtoolError("The perf recipe does not actually check out "
303                           "source and thus cannot be supported by this tool",
304                           4)
305
306    if pn in ['kernel-devsrc', 'package-index'] or pn.startswith('gcc-source'):
307        raise DevtoolError("The %s recipe is not supported by this tool" % pn, 4)
308
309    if bb.data.inherits_class('image', d):
310        raise DevtoolError("The %s recipe is an image, and therefore is not "
311                           "supported by this tool" % pn, 4)
312
313    if bb.data.inherits_class('populate_sdk', d):
314        raise DevtoolError("The %s recipe is an SDK, and therefore is not "
315                           "supported by this tool" % pn, 4)
316
317    if bb.data.inherits_class('packagegroup', d):
318        raise DevtoolError("The %s recipe is a packagegroup, and therefore is "
319                           "not supported by this tool" % pn, 4)
320
321    if bb.data.inherits_class('externalsrc', d) and d.getVar('EXTERNALSRC'):
322        # Not an incompatibility error per se, so we don't pass the error code
323        raise DevtoolError("externalsrc is currently enabled for the %s "
324                           "recipe. This prevents the normal do_patch task "
325                           "from working. You will need to disable this "
326                           "first." % pn)
327
328def _dry_run_copy(src, dst, dry_run_outdir, base_outdir):
329    """Common function for copying a file to the dry run output directory"""
330    relpath = os.path.relpath(dst, base_outdir)
331    if relpath.startswith('..'):
332        raise Exception('Incorrect base path %s for path %s' % (base_outdir, dst))
333    dst = os.path.join(dry_run_outdir, relpath)
334    dst_d = os.path.dirname(dst)
335    if dst_d:
336        bb.utils.mkdirhier(dst_d)
337    # Don't overwrite existing files, otherwise in the case of an upgrade
338    # the dry-run written out recipe will be overwritten with an unmodified
339    # version
340    if not os.path.exists(dst):
341        shutil.copy(src, dst)
342
343def _move_file(src, dst, dry_run_outdir=None, base_outdir=None):
344    """Move a file. Creates all the directory components of destination path."""
345    dry_run_suffix = ' (dry-run)' if dry_run_outdir else ''
346    logger.debug('Moving %s to %s%s' % (src, dst, dry_run_suffix))
347    if dry_run_outdir:
348        # We want to copy here, not move
349        _dry_run_copy(src, dst, dry_run_outdir, base_outdir)
350    else:
351        dst_d = os.path.dirname(dst)
352        if dst_d:
353            bb.utils.mkdirhier(dst_d)
354        shutil.move(src, dst)
355
356def _copy_file(src, dst, dry_run_outdir=None, base_outdir=None):
357    """Copy a file. Creates all the directory components of destination path."""
358    dry_run_suffix = ' (dry-run)' if dry_run_outdir else ''
359    logger.debug('Copying %s to %s%s' % (src, dst, dry_run_suffix))
360    if dry_run_outdir:
361        _dry_run_copy(src, dst, dry_run_outdir, base_outdir)
362    else:
363        dst_d = os.path.dirname(dst)
364        if dst_d:
365            bb.utils.mkdirhier(dst_d)
366        shutil.copy(src, dst)
367
368def _git_ls_tree(repodir, treeish='HEAD', recursive=False):
369    """List contents of a git treeish"""
370    import bb
371    cmd = ['git', 'ls-tree', '-z', treeish]
372    if recursive:
373        cmd.append('-r')
374    out, _ = bb.process.run(cmd, cwd=repodir)
375    ret = {}
376    if out:
377        for line in out.split('\0'):
378            if line:
379                split = line.split(None, 4)
380                ret[split[3]] = split[0:3]
381    return ret
382
383def _git_exclude_path(srctree, path):
384    """Return pathspec (list of paths) that excludes certain path"""
385    # NOTE: "Filtering out" files/paths in this way is not entirely reliable -
386    # we don't catch files that are deleted, for example. A more reliable way
387    # to implement this would be to use "negative pathspecs" which were
388    # introduced in Git v1.9.0. Revisit this when/if the required Git version
389    # becomes greater than that.
390    path = os.path.normpath(path)
391    recurse = True if len(path.split(os.path.sep)) > 1 else False
392    git_files = list(_git_ls_tree(srctree, 'HEAD', recurse).keys())
393    if path in git_files:
394        git_files.remove(path)
395        return git_files
396    else:
397        return ['.']
398
399def _ls_tree(directory):
400    """Recursive listing of files in a directory"""
401    ret = []
402    for root, dirs, files in os.walk(directory):
403        ret.extend([os.path.relpath(os.path.join(root, fname), directory) for
404                    fname in files])
405    return ret
406
407
408def extract(args, config, basepath, workspace):
409    """Entry point for the devtool 'extract' subcommand"""
410    import bb
411
412    tinfoil = setup_tinfoil(basepath=basepath, tracking=True)
413    if not tinfoil:
414        # Error already shown
415        return 1
416    try:
417        rd = parse_recipe(config, tinfoil, args.recipename, True)
418        if not rd:
419            return 1
420
421        srctree = os.path.abspath(args.srctree)
422        initial_rev, _ = _extract_source(srctree, args.keep_temp, args.branch, False, config, basepath, workspace, args.fixed_setup, rd, tinfoil, no_overrides=args.no_overrides)
423        logger.info('Source tree extracted to %s' % srctree)
424
425        if initial_rev:
426            return 0
427        else:
428            return 1
429    finally:
430        tinfoil.shutdown()
431
432def sync(args, config, basepath, workspace):
433    """Entry point for the devtool 'sync' subcommand"""
434    import bb
435
436    tinfoil = setup_tinfoil(basepath=basepath, tracking=True)
437    if not tinfoil:
438        # Error already shown
439        return 1
440    try:
441        rd = parse_recipe(config, tinfoil, args.recipename, True)
442        if not rd:
443            return 1
444
445        srctree = os.path.abspath(args.srctree)
446        initial_rev, _ = _extract_source(srctree, args.keep_temp, args.branch, True, config, basepath, workspace, args.fixed_setup, rd, tinfoil, no_overrides=True)
447        logger.info('Source tree %s synchronized' % srctree)
448
449        if initial_rev:
450            return 0
451        else:
452            return 1
453    finally:
454        tinfoil.shutdown()
455
456def symlink_oelocal_files_srctree(rd,srctree):
457    import oe.patch
458    if os.path.abspath(rd.getVar('S')) == os.path.abspath(rd.getVar('WORKDIR')):
459        # If recipe extracts to ${WORKDIR}, symlink the files into the srctree
460        # (otherwise the recipe won't build as expected)
461        local_files_dir = os.path.join(srctree, 'oe-local-files')
462        addfiles = []
463        for root, _, files in os.walk(local_files_dir):
464            relpth = os.path.relpath(root, local_files_dir)
465            if relpth != '.':
466                bb.utils.mkdirhier(os.path.join(srctree, relpth))
467            for fn in files:
468                if fn == '.gitignore':
469                    continue
470                destpth = os.path.join(srctree, relpth, fn)
471                if os.path.exists(destpth):
472                    os.unlink(destpth)
473                if relpth != '.':
474                    back_relpth = os.path.relpath(local_files_dir, root)
475                    os.symlink('%s/oe-local-files/%s/%s' % (back_relpth, relpth, fn), destpth)
476                else:
477                    os.symlink('oe-local-files/%s' % fn, destpth)
478                addfiles.append(os.path.join(relpth, fn))
479        if addfiles:
480            bb.process.run('git add %s' % ' '.join(addfiles), cwd=srctree)
481            useroptions = []
482            oe.patch.GitApplyTree.gitCommandUserOptions(useroptions, d=rd)
483            bb.process.run('git %s commit -m "Committing local file symlinks\n\n%s"' % (' '.join(useroptions), oe.patch.GitApplyTree.ignore_commit_prefix), cwd=srctree)
484
485
486def _extract_source(srctree, keep_temp, devbranch, sync, config, basepath, workspace, fixed_setup, d, tinfoil, no_overrides=False):
487    """Extract sources of a recipe"""
488    import oe.recipeutils
489    import oe.patch
490    import oe.path
491
492    pn = d.getVar('PN')
493
494    _check_compatible_recipe(pn, d)
495
496    if sync:
497        if not os.path.exists(srctree):
498                raise DevtoolError("output path %s does not exist" % srctree)
499    else:
500        if os.path.exists(srctree):
501            if not os.path.isdir(srctree):
502                raise DevtoolError("output path %s exists and is not a directory" %
503                                   srctree)
504            elif os.listdir(srctree):
505                raise DevtoolError("output path %s already exists and is "
506                                   "non-empty" % srctree)
507
508        if 'noexec' in (d.getVarFlags('do_unpack', False) or []):
509            raise DevtoolError("The %s recipe has do_unpack disabled, unable to "
510                               "extract source" % pn, 4)
511
512    if not sync:
513        # Prepare for shutil.move later on
514        bb.utils.mkdirhier(srctree)
515        os.rmdir(srctree)
516
517    extra_overrides = []
518    if not no_overrides:
519        history = d.varhistory.variable('SRC_URI')
520        for event in history:
521            if not 'flag' in event:
522                if event['op'].startswith((':append[', ':prepend[')):
523                    override = event['op'].split('[')[1].split(']')[0]
524                    if not override.startswith('pn-'):
525                        extra_overrides.append(override)
526        # We want to remove duplicate overrides. If a recipe had multiple
527        # SRC_URI_override += values it would cause mulitple instances of
528        # overrides. This doesn't play nicely with things like creating a
529        # branch for every instance of DEVTOOL_EXTRA_OVERRIDES.
530        extra_overrides = list(set(extra_overrides))
531        if extra_overrides:
532            logger.info('SRC_URI contains some conditional appends/prepends - will create branches to represent these')
533
534    initial_rev = None
535
536    recipefile = d.getVar('FILE')
537    appendfile = recipe_to_append(recipefile, config)
538    is_kernel_yocto = bb.data.inherits_class('kernel-yocto', d)
539
540    # We need to redirect WORKDIR, STAMPS_DIR etc. under a temporary
541    # directory so that:
542    # (a) we pick up all files that get unpacked to the WORKDIR, and
543    # (b) we don't disturb the existing build
544    # However, with recipe-specific sysroots the sysroots for the recipe
545    # will be prepared under WORKDIR, and if we used the system temporary
546    # directory (i.e. usually /tmp) as used by mkdtemp by default, then
547    # our attempts to hardlink files into the recipe-specific sysroots
548    # will fail on systems where /tmp is a different filesystem, and it
549    # would have to fall back to copying the files which is a waste of
550    # time. Put the temp directory under the WORKDIR to prevent that from
551    # being a problem.
552    tempbasedir = d.getVar('WORKDIR')
553    bb.utils.mkdirhier(tempbasedir)
554    tempdir = tempfile.mkdtemp(prefix='devtooltmp-', dir=tempbasedir)
555    try:
556        tinfoil.logger.setLevel(logging.WARNING)
557
558        # FIXME this results in a cache reload under control of tinfoil, which is fine
559        # except we don't get the knotty progress bar
560
561        if os.path.exists(appendfile):
562            appendbackup = os.path.join(tempdir, os.path.basename(appendfile) + '.bak')
563            shutil.copyfile(appendfile, appendbackup)
564        else:
565            appendbackup = None
566            bb.utils.mkdirhier(os.path.dirname(appendfile))
567        logger.debug('writing append file %s' % appendfile)
568        with open(appendfile, 'a') as f:
569            f.write('###--- _extract_source\n')
570            f.write('ERROR_QA:remove = "patch-fuzz"\n')
571            f.write('DEVTOOL_TEMPDIR = "%s"\n' % tempdir)
572            f.write('DEVTOOL_DEVBRANCH = "%s"\n' % devbranch)
573            if not is_kernel_yocto:
574                f.write('PATCHTOOL = "git"\n')
575                f.write('PATCH_COMMIT_FUNCTIONS = "1"\n')
576            if extra_overrides:
577                f.write('DEVTOOL_EXTRA_OVERRIDES = "%s"\n' % ':'.join(extra_overrides))
578            f.write('inherit devtool-source\n')
579            f.write('###--- _extract_source\n')
580
581        update_unlockedsigs(basepath, workspace, fixed_setup, [pn])
582
583        sstate_manifests = d.getVar('SSTATE_MANIFESTS')
584        bb.utils.mkdirhier(sstate_manifests)
585        preservestampfile = os.path.join(sstate_manifests, 'preserve-stamps')
586        with open(preservestampfile, 'w') as f:
587            f.write(d.getVar('STAMP'))
588        try:
589            if is_kernel_yocto:
590                # We need to generate the kernel config
591                task = 'do_configure'
592            else:
593                task = 'do_patch'
594
595                if 'noexec' in (d.getVarFlags(task, False) or []) or 'task' not in (d.getVarFlags(task, False) or []):
596                    logger.info('The %s recipe has %s disabled. Running only '
597                                       'do_configure task dependencies' % (pn, task))
598
599                    if 'depends' in d.getVarFlags('do_configure', False):
600                        pn = d.getVarFlags('do_configure', False)['depends']
601                        pn = pn.replace('${PV}', d.getVar('PV'))
602                        pn = pn.replace('${COMPILERDEP}', d.getVar('COMPILERDEP'))
603                        task = None
604
605            # Run the fetch + unpack tasks
606            res = tinfoil.build_targets(pn,
607                                        task,
608                                        handle_events=True)
609        finally:
610            if os.path.exists(preservestampfile):
611                os.remove(preservestampfile)
612
613        if not res:
614            raise DevtoolError('Extracting source for %s failed' % pn)
615
616        if not is_kernel_yocto and ('noexec' in (d.getVarFlags('do_patch', False) or []) or 'task' not in (d.getVarFlags('do_patch', False) or [])):
617            workshareddir = d.getVar('S')
618            if os.path.islink(srctree):
619                os.unlink(srctree)
620
621            os.symlink(workshareddir, srctree)
622
623            # The initial_rev file is created in devtool_post_unpack function that will not be executed if
624            # do_unpack/do_patch tasks are disabled so we have to directly say that source extraction was successful
625            return True, True
626
627        try:
628            with open(os.path.join(tempdir, 'initial_rev'), 'r') as f:
629                initial_rev = f.read()
630
631            with open(os.path.join(tempdir, 'srcsubdir'), 'r') as f:
632                srcsubdir = f.read()
633        except FileNotFoundError as e:
634            raise DevtoolError('Something went wrong with source extraction - the devtool-source class was not active or did not function correctly:\n%s' % str(e))
635        srcsubdir_rel = os.path.relpath(srcsubdir, os.path.join(tempdir, 'workdir'))
636
637        # Check if work-shared is empty, if yes
638        # find source and copy to work-shared
639        if is_kernel_yocto:
640            workshareddir = d.getVar('STAGING_KERNEL_DIR')
641            staging_kerVer = get_staging_kver(workshareddir)
642            kernelVersion = d.getVar('LINUX_VERSION')
643
644            # handle dangling symbolic link in work-shared:
645            if os.path.islink(workshareddir):
646                os.unlink(workshareddir)
647
648            if os.path.exists(workshareddir) and (not os.listdir(workshareddir) or kernelVersion != staging_kerVer):
649                shutil.rmtree(workshareddir)
650                oe.path.copyhardlinktree(srcsubdir,workshareddir)
651            elif not os.path.exists(workshareddir):
652                oe.path.copyhardlinktree(srcsubdir,workshareddir)
653
654        tempdir_localdir = os.path.join(tempdir, 'oe-local-files')
655        srctree_localdir = os.path.join(srctree, 'oe-local-files')
656
657        if sync:
658            bb.process.run('git fetch file://' + srcsubdir + ' ' + devbranch + ':' + devbranch, cwd=srctree)
659
660            # Move oe-local-files directory to srctree
661            # As the oe-local-files is not part of the constructed git tree,
662            # remove them directly during the synchrounizating might surprise
663            # the users.  Instead, we move it to oe-local-files.bak and remind
664            # user in the log message.
665            if os.path.exists(srctree_localdir + '.bak'):
666                shutil.rmtree(srctree_localdir, srctree_localdir + '.bak')
667
668            if os.path.exists(srctree_localdir):
669                logger.info('Backing up current local file directory %s' % srctree_localdir)
670                shutil.move(srctree_localdir, srctree_localdir + '.bak')
671
672            if os.path.exists(tempdir_localdir):
673                logger.info('Syncing local source files to srctree...')
674                shutil.copytree(tempdir_localdir, srctree_localdir)
675        else:
676            # Move oe-local-files directory to srctree
677            if os.path.exists(tempdir_localdir):
678                logger.info('Adding local source files to srctree...')
679                shutil.move(tempdir_localdir, srcsubdir)
680
681            shutil.move(srcsubdir, srctree)
682            symlink_oelocal_files_srctree(d,srctree)
683
684        if is_kernel_yocto:
685            logger.info('Copying kernel config to srctree')
686            shutil.copy2(os.path.join(tempdir, '.config'), srctree)
687
688    finally:
689        if appendbackup:
690            shutil.copyfile(appendbackup, appendfile)
691        elif os.path.exists(appendfile):
692            os.remove(appendfile)
693        if keep_temp:
694            logger.info('Preserving temporary directory %s' % tempdir)
695        else:
696            shutil.rmtree(tempdir)
697    return initial_rev, srcsubdir_rel
698
699def _add_md5(config, recipename, filename):
700    """Record checksum of a file (or recursively for a directory) to the md5-file of the workspace"""
701    import bb.utils
702
703    def addfile(fn):
704        md5 = bb.utils.md5_file(fn)
705        with open(os.path.join(config.workspace_path, '.devtool_md5'), 'a+') as f:
706            md5_str = '%s|%s|%s\n' % (recipename, os.path.relpath(fn, config.workspace_path), md5)
707            f.seek(0, os.SEEK_SET)
708            if not md5_str in f.read():
709                f.write(md5_str)
710
711    if os.path.isdir(filename):
712        for root, _, files in os.walk(filename):
713            for f in files:
714                addfile(os.path.join(root, f))
715    else:
716        addfile(filename)
717
718def _check_preserve(config, recipename):
719    """Check if a file was manually changed and needs to be saved in 'attic'
720       directory"""
721    import bb.utils
722    origfile = os.path.join(config.workspace_path, '.devtool_md5')
723    newfile = os.path.join(config.workspace_path, '.devtool_md5_new')
724    preservepath = os.path.join(config.workspace_path, 'attic', recipename)
725    with open(origfile, 'r') as f:
726        with open(newfile, 'w') as tf:
727            for line in f.readlines():
728                splitline = line.rstrip().split('|')
729                if splitline[0] == recipename:
730                    removefile = os.path.join(config.workspace_path, splitline[1])
731                    try:
732                        md5 = bb.utils.md5_file(removefile)
733                    except IOError as err:
734                        if err.errno == 2:
735                            # File no longer exists, skip it
736                            continue
737                        else:
738                            raise
739                    if splitline[2] != md5:
740                        bb.utils.mkdirhier(preservepath)
741                        preservefile = os.path.basename(removefile)
742                        logger.warning('File %s modified since it was written, preserving in %s' % (preservefile, preservepath))
743                        shutil.move(removefile, os.path.join(preservepath, preservefile))
744                    else:
745                        os.remove(removefile)
746                else:
747                    tf.write(line)
748    bb.utils.rename(newfile, origfile)
749
750def get_staging_kver(srcdir):
751    # Kernel version from work-shared
752    kerver = []
753    staging_kerVer=""
754    if os.path.exists(srcdir) and os.listdir(srcdir):
755        with open(os.path.join(srcdir,"Makefile")) as f:
756            version = [next(f) for x in range(5)][1:4]
757            for word in version:
758                kerver.append(word.split('= ')[1].split('\n')[0])
759            staging_kerVer = ".".join(kerver)
760    return staging_kerVer
761
762def get_staging_kbranch(srcdir):
763    staging_kbranch = ""
764    if os.path.exists(srcdir) and os.listdir(srcdir):
765        (branch, _) = bb.process.run('git branch | grep \* | cut -d \' \' -f2', cwd=srcdir)
766        staging_kbranch = "".join(branch.split('\n')[0])
767    return staging_kbranch
768
769def get_real_srctree(srctree, s, workdir):
770    # Check that recipe isn't using a shared workdir
771    s = os.path.abspath(s)
772    workdir = os.path.abspath(workdir)
773    if s.startswith(workdir) and s != workdir and os.path.dirname(s) != workdir:
774        # Handle if S is set to a subdirectory of the source
775        srcsubdir = os.path.relpath(s, workdir).split(os.sep, 1)[1]
776        srctree = os.path.join(srctree, srcsubdir)
777    return srctree
778
779def modify(args, config, basepath, workspace):
780    """Entry point for the devtool 'modify' subcommand"""
781    import bb
782    import oe.recipeutils
783    import oe.patch
784    import oe.path
785
786    if args.recipename in workspace:
787        raise DevtoolError("recipe %s is already in your workspace" %
788                           args.recipename)
789
790    tinfoil = setup_tinfoil(basepath=basepath, tracking=True)
791    try:
792        rd = parse_recipe(config, tinfoil, args.recipename, True)
793        if not rd:
794            return 1
795
796        pn = rd.getVar('PN')
797        if pn != args.recipename:
798            logger.info('Mapping %s to %s' % (args.recipename, pn))
799        if pn in workspace:
800            raise DevtoolError("recipe %s is already in your workspace" %
801                            pn)
802
803        if args.srctree:
804            srctree = os.path.abspath(args.srctree)
805        else:
806            srctree = get_default_srctree(config, pn)
807
808        if args.no_extract and not os.path.isdir(srctree):
809            raise DevtoolError("--no-extract specified and source path %s does "
810                            "not exist or is not a directory" %
811                            srctree)
812
813        recipefile = rd.getVar('FILE')
814        appendfile = recipe_to_append(recipefile, config, args.wildcard)
815        if os.path.exists(appendfile):
816            raise DevtoolError("Another variant of recipe %s is already in your "
817                            "workspace (only one variant of a recipe can "
818                            "currently be worked on at once)"
819                            % pn)
820
821        _check_compatible_recipe(pn, rd)
822
823        initial_rev = None
824        commits = []
825        check_commits = False
826
827        if bb.data.inherits_class('kernel-yocto', rd):
828            # Current set kernel version
829            kernelVersion = rd.getVar('LINUX_VERSION')
830            srcdir = rd.getVar('STAGING_KERNEL_DIR')
831            kbranch = rd.getVar('KBRANCH')
832
833            staging_kerVer = get_staging_kver(srcdir)
834            staging_kbranch = get_staging_kbranch(srcdir)
835            if (os.path.exists(srcdir) and os.listdir(srcdir)) and (kernelVersion in staging_kerVer and staging_kbranch == kbranch):
836                oe.path.copyhardlinktree(srcdir,srctree)
837                workdir = rd.getVar('WORKDIR')
838                srcsubdir = rd.getVar('S')
839                localfilesdir = os.path.join(srctree,'oe-local-files')
840                # Move local source files into separate subdir
841                recipe_patches = [os.path.basename(patch) for patch in oe.recipeutils.get_recipe_patches(rd)]
842                local_files = oe.recipeutils.get_recipe_local_files(rd)
843
844                for key in local_files.copy():
845                    if key.endswith('scc'):
846                        sccfile = open(local_files[key], 'r')
847                        for l in sccfile:
848                            line = l.split()
849                            if line and line[0] in ('kconf', 'patch'):
850                                cfg = os.path.join(os.path.dirname(local_files[key]), line[-1])
851                                if not cfg in local_files.values():
852                                    local_files[line[-1]] = cfg
853                                    shutil.copy2(cfg, workdir)
854                        sccfile.close()
855
856                # Ignore local files with subdir={BP}
857                srcabspath = os.path.abspath(srcsubdir)
858                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))]
859                if local_files:
860                    for fname in local_files:
861                        _move_file(os.path.join(workdir, fname), os.path.join(srctree, 'oe-local-files', fname))
862                    with open(os.path.join(srctree, 'oe-local-files', '.gitignore'), 'w') as f:
863                        f.write('# Ignore local files, by default. Remove this file ''if you want to commit the directory to Git\n*\n')
864
865                symlink_oelocal_files_srctree(rd,srctree)
866
867                task = 'do_configure'
868                res = tinfoil.build_targets(pn, task, handle_events=True)
869
870                # Copy .config to workspace
871                kconfpath = rd.getVar('B')
872                logger.info('Copying kernel config to workspace')
873                shutil.copy2(os.path.join(kconfpath, '.config'),srctree)
874
875                # Set this to true, we still need to get initial_rev
876                # by parsing the git repo
877                args.no_extract = True
878
879        if not args.no_extract:
880            initial_rev, _ = _extract_source(srctree, args.keep_temp, args.branch, False, config, basepath, workspace, args.fixed_setup, rd, tinfoil, no_overrides=args.no_overrides)
881            if not initial_rev:
882                return 1
883            logger.info('Source tree extracted to %s' % srctree)
884            if os.path.exists(os.path.join(srctree, '.git')):
885                # Get list of commits since this revision
886                (stdout, _) = bb.process.run('git rev-list --reverse %s..HEAD' % initial_rev, cwd=srctree)
887                commits = stdout.split()
888                check_commits = True
889        else:
890            if os.path.exists(os.path.join(srctree, '.git')):
891                # Check if it's a tree previously extracted by us. This is done
892                # by ensuring that devtool-base and args.branch (devtool) exist.
893                # The check_commits logic will cause an exception if either one
894                # of these doesn't exist
895                try:
896                    (stdout, _) = bb.process.run('git branch --contains devtool-base', cwd=srctree)
897                    bb.process.run('git rev-parse %s' % args.branch, cwd=srctree)
898                except bb.process.ExecutionError:
899                    stdout = ''
900                if stdout:
901                    check_commits = True
902                for line in stdout.splitlines():
903                    if line.startswith('*'):
904                        (stdout, _) = bb.process.run('git rev-parse devtool-base', cwd=srctree)
905                        initial_rev = stdout.rstrip()
906                if not initial_rev:
907                    # Otherwise, just grab the head revision
908                    (stdout, _) = bb.process.run('git rev-parse HEAD', cwd=srctree)
909                    initial_rev = stdout.rstrip()
910
911        branch_patches = {}
912        if check_commits:
913            # Check if there are override branches
914            (stdout, _) = bb.process.run('git branch', cwd=srctree)
915            branches = []
916            for line in stdout.rstrip().splitlines():
917                branchname = line[2:].rstrip()
918                if branchname.startswith(override_branch_prefix):
919                    branches.append(branchname)
920            if branches:
921                logger.warning('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))
922            branches.insert(0, args.branch)
923            seen_patches = []
924            for branch in branches:
925                branch_patches[branch] = []
926                (stdout, _) = bb.process.run('git log devtool-base..%s' % branch, cwd=srctree)
927                for line in stdout.splitlines():
928                    line = line.strip()
929                    if line.startswith(oe.patch.GitApplyTree.patch_line_prefix):
930                        origpatch = line[len(oe.patch.GitApplyTree.patch_line_prefix):].split(':', 1)[-1].strip()
931                        if not origpatch in seen_patches:
932                            seen_patches.append(origpatch)
933                            branch_patches[branch].append(origpatch)
934
935        # Need to grab this here in case the source is within a subdirectory
936        srctreebase = srctree
937        srctree = get_real_srctree(srctree, rd.getVar('S'), rd.getVar('WORKDIR'))
938
939        bb.utils.mkdirhier(os.path.dirname(appendfile))
940        with open(appendfile, 'w') as f:
941            f.write('FILESEXTRAPATHS:prepend := "${THISDIR}/${PN}:"\n')
942            # Local files can be modified/tracked in separate subdir under srctree
943            # Mostly useful for packages with S != WORKDIR
944            f.write('FILESPATH:prepend := "%s:"\n' %
945                    os.path.join(srctreebase, 'oe-local-files'))
946            f.write('# srctreebase: %s\n' % srctreebase)
947
948            f.write('\ninherit externalsrc\n')
949            f.write('# NOTE: We use pn- overrides here to avoid affecting multiple variants in the case where the recipe uses BBCLASSEXTEND\n')
950            f.write('EXTERNALSRC:pn-%s = "%s"\n' % (pn, srctree))
951
952            b_is_s = use_external_build(args.same_dir, args.no_same_dir, rd)
953            if b_is_s:
954                f.write('EXTERNALSRC_BUILD:pn-%s = "%s"\n' % (pn, srctree))
955
956            if bb.data.inherits_class('kernel', rd):
957                f.write('SRCTREECOVEREDTASKS = "do_validate_branches do_kernel_checkout '
958                        'do_fetch do_unpack do_kernel_configcheck"\n')
959                f.write('\ndo_patch[noexec] = "1"\n')
960                f.write('\ndo_configure:append() {\n'
961                        '    cp ${B}/.config ${S}/.config.baseline\n'
962                        '    ln -sfT ${B}/.config ${S}/.config.new\n'
963                        '}\n')
964                f.write('\ndo_kernel_configme:prepend() {\n'
965                        '    if [ -e ${S}/.config ]; then\n'
966                        '        mv ${S}/.config ${S}/.config.old\n'
967                        '    fi\n'
968                        '}\n')
969            if rd.getVarFlag('do_menuconfig','task'):
970                f.write('\ndo_configure:append() {\n'
971                '    if [ ${@ oe.types.boolean(\'${KCONFIG_CONFIG_ENABLE_MENUCONFIG}\') } = True ]; then\n'
972                '        cp ${KCONFIG_CONFIG_ROOTDIR}/.config ${S}/.config.baseline\n'
973                '        ln -sfT ${KCONFIG_CONFIG_ROOTDIR}/.config ${S}/.config.new\n'
974                '    fi\n'
975                '}\n')
976            if initial_rev:
977                f.write('\n# initial_rev: %s\n' % initial_rev)
978                for commit in commits:
979                    f.write('# commit: %s\n' % commit)
980            if branch_patches:
981                for branch in branch_patches:
982                    if branch == args.branch:
983                        continue
984                    f.write('# patches_%s: %s\n' % (branch, ','.join(branch_patches[branch])))
985
986        update_unlockedsigs(basepath, workspace, args.fixed_setup, [pn])
987
988        _add_md5(config, pn, appendfile)
989
990        logger.info('Recipe %s now set up to build from %s' % (pn, srctree))
991
992    finally:
993        tinfoil.shutdown()
994
995    return 0
996
997
998def rename(args, config, basepath, workspace):
999    """Entry point for the devtool 'rename' subcommand"""
1000    import bb
1001    import oe.recipeutils
1002
1003    check_workspace_recipe(workspace, args.recipename)
1004
1005    if not (args.newname or args.version):
1006        raise DevtoolError('You must specify a new name, a version with -V/--version, or both')
1007
1008    recipefile = workspace[args.recipename]['recipefile']
1009    if not recipefile:
1010        raise DevtoolError('devtool rename can only be used where the recipe file itself is in the workspace (e.g. after devtool add)')
1011
1012    if args.newname and args.newname != args.recipename:
1013        reason = oe.recipeutils.validate_pn(args.newname)
1014        if reason:
1015            raise DevtoolError(reason)
1016        newname = args.newname
1017    else:
1018        newname = args.recipename
1019
1020    append = workspace[args.recipename]['bbappend']
1021    appendfn = os.path.splitext(os.path.basename(append))[0]
1022    splitfn = appendfn.split('_')
1023    if len(splitfn) > 1:
1024        origfnver = appendfn.split('_')[1]
1025    else:
1026        origfnver = ''
1027
1028    recipefilemd5 = None
1029    tinfoil = setup_tinfoil(basepath=basepath, tracking=True)
1030    try:
1031        rd = parse_recipe(config, tinfoil, args.recipename, True)
1032        if not rd:
1033            return 1
1034
1035        bp = rd.getVar('BP')
1036        bpn = rd.getVar('BPN')
1037        if newname != args.recipename:
1038            localdata = rd.createCopy()
1039            localdata.setVar('PN', newname)
1040            newbpn = localdata.getVar('BPN')
1041        else:
1042            newbpn = bpn
1043        s = rd.getVar('S', False)
1044        src_uri = rd.getVar('SRC_URI', False)
1045        pv = rd.getVar('PV')
1046
1047        # Correct variable values that refer to the upstream source - these
1048        # values must stay the same, so if the name/version are changing then
1049        # we need to fix them up
1050        new_s = s
1051        new_src_uri = src_uri
1052        if newbpn != bpn:
1053            # ${PN} here is technically almost always incorrect, but people do use it
1054            new_s = new_s.replace('${BPN}', bpn)
1055            new_s = new_s.replace('${PN}', bpn)
1056            new_s = new_s.replace('${BP}', '%s-${PV}' % bpn)
1057            new_src_uri = new_src_uri.replace('${BPN}', bpn)
1058            new_src_uri = new_src_uri.replace('${PN}', bpn)
1059            new_src_uri = new_src_uri.replace('${BP}', '%s-${PV}' % bpn)
1060        if args.version and origfnver == pv:
1061            new_s = new_s.replace('${PV}', pv)
1062            new_s = new_s.replace('${BP}', '${BPN}-%s' % pv)
1063            new_src_uri = new_src_uri.replace('${PV}', pv)
1064            new_src_uri = new_src_uri.replace('${BP}', '${BPN}-%s' % pv)
1065        patchfields = {}
1066        if new_s != s:
1067            patchfields['S'] = new_s
1068        if new_src_uri != src_uri:
1069            patchfields['SRC_URI'] = new_src_uri
1070        if patchfields:
1071            recipefilemd5 = bb.utils.md5_file(recipefile)
1072            oe.recipeutils.patch_recipe(rd, recipefile, patchfields)
1073            newrecipefilemd5 = bb.utils.md5_file(recipefile)
1074    finally:
1075        tinfoil.shutdown()
1076
1077    if args.version:
1078        newver = args.version
1079    else:
1080        newver = origfnver
1081
1082    if newver:
1083        newappend = '%s_%s.bbappend' % (newname, newver)
1084        newfile =  '%s_%s.bb' % (newname, newver)
1085    else:
1086        newappend = '%s.bbappend' % newname
1087        newfile = '%s.bb' % newname
1088
1089    oldrecipedir = os.path.dirname(recipefile)
1090    newrecipedir = os.path.join(config.workspace_path, 'recipes', newname)
1091    if oldrecipedir != newrecipedir:
1092        bb.utils.mkdirhier(newrecipedir)
1093
1094    newappend = os.path.join(os.path.dirname(append), newappend)
1095    newfile = os.path.join(newrecipedir, newfile)
1096
1097    # Rename bbappend
1098    logger.info('Renaming %s to %s' % (append, newappend))
1099    bb.utils.rename(append, newappend)
1100    # Rename recipe file
1101    logger.info('Renaming %s to %s' % (recipefile, newfile))
1102    bb.utils.rename(recipefile, newfile)
1103
1104    # Rename source tree if it's the default path
1105    appendmd5 = None
1106    if not args.no_srctree:
1107        srctree = workspace[args.recipename]['srctree']
1108        if os.path.abspath(srctree) == os.path.join(config.workspace_path, 'sources', args.recipename):
1109            newsrctree = os.path.join(config.workspace_path, 'sources', newname)
1110            logger.info('Renaming %s to %s' % (srctree, newsrctree))
1111            shutil.move(srctree, newsrctree)
1112            # Correct any references (basically EXTERNALSRC*) in the .bbappend
1113            appendmd5 = bb.utils.md5_file(newappend)
1114            appendlines = []
1115            with open(newappend, 'r') as f:
1116                for line in f:
1117                    appendlines.append(line)
1118            with open(newappend, 'w') as f:
1119                for line in appendlines:
1120                    if srctree in line:
1121                        line = line.replace(srctree, newsrctree)
1122                    f.write(line)
1123            newappendmd5 = bb.utils.md5_file(newappend)
1124
1125    bpndir = None
1126    newbpndir = None
1127    if newbpn != bpn:
1128        bpndir = os.path.join(oldrecipedir, bpn)
1129        if os.path.exists(bpndir):
1130            newbpndir = os.path.join(newrecipedir, newbpn)
1131            logger.info('Renaming %s to %s' % (bpndir, newbpndir))
1132            shutil.move(bpndir, newbpndir)
1133
1134    bpdir = None
1135    newbpdir = None
1136    if newver != origfnver or newbpn != bpn:
1137        bpdir = os.path.join(oldrecipedir, bp)
1138        if os.path.exists(bpdir):
1139            newbpdir = os.path.join(newrecipedir, '%s-%s' % (newbpn, newver))
1140            logger.info('Renaming %s to %s' % (bpdir, newbpdir))
1141            shutil.move(bpdir, newbpdir)
1142
1143    if oldrecipedir != newrecipedir:
1144        # Move any stray files and delete the old recipe directory
1145        for entry in os.listdir(oldrecipedir):
1146            oldpath = os.path.join(oldrecipedir, entry)
1147            newpath = os.path.join(newrecipedir, entry)
1148            logger.info('Renaming %s to %s' % (oldpath, newpath))
1149            shutil.move(oldpath, newpath)
1150        os.rmdir(oldrecipedir)
1151
1152    # Now take care of entries in .devtool_md5
1153    md5entries = []
1154    with open(os.path.join(config.workspace_path, '.devtool_md5'), 'r') as f:
1155        for line in f:
1156            md5entries.append(line)
1157
1158    if bpndir and newbpndir:
1159        relbpndir = os.path.relpath(bpndir, config.workspace_path) + '/'
1160    else:
1161        relbpndir = None
1162    if bpdir and newbpdir:
1163        relbpdir = os.path.relpath(bpdir, config.workspace_path) + '/'
1164    else:
1165        relbpdir = None
1166
1167    with open(os.path.join(config.workspace_path, '.devtool_md5'), 'w') as f:
1168        for entry in md5entries:
1169            splitentry = entry.rstrip().split('|')
1170            if len(splitentry) > 2:
1171                if splitentry[0] == args.recipename:
1172                    splitentry[0] = newname
1173                    if splitentry[1] == os.path.relpath(append, config.workspace_path):
1174                        splitentry[1] = os.path.relpath(newappend, config.workspace_path)
1175                        if appendmd5 and splitentry[2] == appendmd5:
1176                            splitentry[2] = newappendmd5
1177                    elif splitentry[1] == os.path.relpath(recipefile, config.workspace_path):
1178                        splitentry[1] = os.path.relpath(newfile, config.workspace_path)
1179                        if recipefilemd5 and splitentry[2] == recipefilemd5:
1180                            splitentry[2] = newrecipefilemd5
1181                    elif relbpndir and splitentry[1].startswith(relbpndir):
1182                        splitentry[1] = os.path.relpath(os.path.join(newbpndir, splitentry[1][len(relbpndir):]), config.workspace_path)
1183                    elif relbpdir and splitentry[1].startswith(relbpdir):
1184                        splitentry[1] = os.path.relpath(os.path.join(newbpdir, splitentry[1][len(relbpdir):]), config.workspace_path)
1185                    entry = '|'.join(splitentry) + '\n'
1186            f.write(entry)
1187    return 0
1188
1189
1190def _get_patchset_revs(srctree, recipe_path, initial_rev=None, force_patch_refresh=False):
1191    """Get initial and update rev of a recipe. These are the start point of the
1192    whole patchset and start point for the patches to be re-generated/updated.
1193    """
1194    import bb
1195
1196    # Get current branch
1197    stdout, _ = bb.process.run('git rev-parse --abbrev-ref HEAD',
1198                               cwd=srctree)
1199    branchname = stdout.rstrip()
1200
1201    # Parse initial rev from recipe if not specified
1202    commits = []
1203    patches = []
1204    with open(recipe_path, 'r') as f:
1205        for line in f:
1206            if line.startswith('# initial_rev:'):
1207                if not initial_rev:
1208                    initial_rev = line.split(':')[-1].strip()
1209            elif line.startswith('# commit:') and not force_patch_refresh:
1210                commits.append(line.split(':')[-1].strip())
1211            elif line.startswith('# patches_%s:' % branchname):
1212                patches = line.split(':')[-1].strip().split(',')
1213
1214    update_rev = initial_rev
1215    changed_revs = None
1216    if initial_rev:
1217        # Find first actually changed revision
1218        stdout, _ = bb.process.run('git rev-list --reverse %s..HEAD' %
1219                                   initial_rev, cwd=srctree)
1220        newcommits = stdout.split()
1221        for i in range(min(len(commits), len(newcommits))):
1222            if newcommits[i] == commits[i]:
1223                update_rev = commits[i]
1224
1225        try:
1226            stdout, _ = bb.process.run('git cherry devtool-patched',
1227                                        cwd=srctree)
1228        except bb.process.ExecutionError as err:
1229            stdout = None
1230
1231        if stdout is not None and not force_patch_refresh:
1232            changed_revs = []
1233            for line in stdout.splitlines():
1234                if line.startswith('+ '):
1235                    rev = line.split()[1]
1236                    if rev in newcommits:
1237                        changed_revs.append(rev)
1238
1239    return initial_rev, update_rev, changed_revs, patches
1240
1241def _remove_file_entries(srcuri, filelist):
1242    """Remove file:// entries from SRC_URI"""
1243    remaining = filelist[:]
1244    entries = []
1245    for fname in filelist:
1246        basename = os.path.basename(fname)
1247        for i in range(len(srcuri)):
1248            if (srcuri[i].startswith('file://') and
1249                    os.path.basename(srcuri[i].split(';')[0]) == basename):
1250                entries.append(srcuri[i])
1251                remaining.remove(fname)
1252                srcuri.pop(i)
1253                break
1254    return entries, remaining
1255
1256def _replace_srcuri_entry(srcuri, filename, newentry):
1257    """Replace entry corresponding to specified file with a new entry"""
1258    basename = os.path.basename(filename)
1259    for i in range(len(srcuri)):
1260        if os.path.basename(srcuri[i].split(';')[0]) == basename:
1261            srcuri.pop(i)
1262            srcuri.insert(i, newentry)
1263            break
1264
1265def _remove_source_files(append, files, destpath, no_report_remove=False, dry_run=False):
1266    """Unlink existing patch files"""
1267
1268    dry_run_suffix = ' (dry-run)' if dry_run else ''
1269
1270    for path in files:
1271        if append:
1272            if not destpath:
1273                raise Exception('destpath should be set here')
1274            path = os.path.join(destpath, os.path.basename(path))
1275
1276        if os.path.exists(path):
1277            if not no_report_remove:
1278                logger.info('Removing file %s%s' % (path, dry_run_suffix))
1279            if not dry_run:
1280                # FIXME "git rm" here would be nice if the file in question is
1281                #       tracked
1282                # FIXME there's a chance that this file is referred to by
1283                #       another recipe, in which case deleting wouldn't be the
1284                #       right thing to do
1285                os.remove(path)
1286                # Remove directory if empty
1287                try:
1288                    os.rmdir(os.path.dirname(path))
1289                except OSError as ose:
1290                    if ose.errno != errno.ENOTEMPTY:
1291                        raise
1292
1293
1294def _export_patches(srctree, rd, start_rev, destdir, changed_revs=None):
1295    """Export patches from srctree to given location.
1296       Returns three-tuple of dicts:
1297         1. updated - patches that already exist in SRCURI
1298         2. added - new patches that don't exist in SRCURI
1299         3  removed - patches that exist in SRCURI but not in exported patches
1300      In each dict the key is the 'basepath' of the URI and value is the
1301      absolute path to the existing file in recipe space (if any).
1302    """
1303    import oe.recipeutils
1304    from oe.patch import GitApplyTree
1305    updated = OrderedDict()
1306    added = OrderedDict()
1307    seqpatch_re = re.compile('^([0-9]{4}-)?(.+)')
1308
1309    existing_patches = dict((os.path.basename(path), path) for path in
1310                            oe.recipeutils.get_recipe_patches(rd))
1311    logger.debug('Existing patches: %s' % existing_patches)
1312
1313    # Generate patches from Git, exclude local files directory
1314    patch_pathspec = _git_exclude_path(srctree, 'oe-local-files')
1315    GitApplyTree.extractPatches(srctree, start_rev, destdir, patch_pathspec)
1316
1317    new_patches = sorted(os.listdir(destdir))
1318    for new_patch in new_patches:
1319        # Strip numbering from patch names. If it's a git sequence named patch,
1320        # the numbers might not match up since we are starting from a different
1321        # revision This does assume that people are using unique shortlog
1322        # values, but they ought to be anyway...
1323        new_basename = seqpatch_re.match(new_patch).group(2)
1324        match_name = None
1325        for old_patch in existing_patches:
1326            old_basename = seqpatch_re.match(old_patch).group(2)
1327            old_basename_splitext = os.path.splitext(old_basename)
1328            if old_basename.endswith(('.gz', '.bz2', '.Z')) and old_basename_splitext[0] == new_basename:
1329                old_patch_noext = os.path.splitext(old_patch)[0]
1330                match_name = old_patch_noext
1331                break
1332            elif new_basename == old_basename:
1333                match_name = old_patch
1334                break
1335        if match_name:
1336            # Rename patch files
1337            if new_patch != match_name:
1338                bb.utils.rename(os.path.join(destdir, new_patch),
1339                          os.path.join(destdir, match_name))
1340            # Need to pop it off the list now before checking changed_revs
1341            oldpath = existing_patches.pop(old_patch)
1342            if changed_revs is not None:
1343                # Avoid updating patches that have not actually changed
1344                with open(os.path.join(destdir, match_name), 'r') as f:
1345                    firstlineitems = f.readline().split()
1346                    # Looking for "From <hash>" line
1347                    if len(firstlineitems) > 1 and len(firstlineitems[1]) == 40:
1348                        if not firstlineitems[1] in changed_revs:
1349                            continue
1350            # Recompress if necessary
1351            if oldpath.endswith(('.gz', '.Z')):
1352                bb.process.run(['gzip', match_name], cwd=destdir)
1353                if oldpath.endswith('.gz'):
1354                    match_name += '.gz'
1355                else:
1356                    match_name += '.Z'
1357            elif oldpath.endswith('.bz2'):
1358                bb.process.run(['bzip2', match_name], cwd=destdir)
1359                match_name += '.bz2'
1360            updated[match_name] = oldpath
1361        else:
1362            added[new_patch] = None
1363    return (updated, added, existing_patches)
1364
1365
1366def _create_kconfig_diff(srctree, rd, outfile):
1367    """Create a kconfig fragment"""
1368    # Only update config fragment if both config files exist
1369    orig_config = os.path.join(srctree, '.config.baseline')
1370    new_config = os.path.join(srctree, '.config.new')
1371    if os.path.exists(orig_config) and os.path.exists(new_config):
1372        cmd = ['diff', '--new-line-format=%L', '--old-line-format=',
1373               '--unchanged-line-format=', orig_config, new_config]
1374        pipe = subprocess.Popen(cmd, stdout=subprocess.PIPE,
1375                                stderr=subprocess.PIPE)
1376        stdout, stderr = pipe.communicate()
1377        if pipe.returncode == 1:
1378            logger.info("Updating config fragment %s" % outfile)
1379            with open(outfile, 'wb') as fobj:
1380                fobj.write(stdout)
1381        elif pipe.returncode == 0:
1382            logger.info("Would remove config fragment %s" % outfile)
1383            if os.path.exists(outfile):
1384                # Remove fragment file in case of empty diff
1385                logger.info("Removing config fragment %s" % outfile)
1386                os.unlink(outfile)
1387        else:
1388            raise bb.process.ExecutionError(cmd, pipe.returncode, stdout, stderr)
1389        return True
1390    return False
1391
1392
1393def _export_local_files(srctree, rd, destdir, srctreebase):
1394    """Copy local files from srctree to given location.
1395       Returns three-tuple of dicts:
1396         1. updated - files that already exist in SRCURI
1397         2. added - new files files that don't exist in SRCURI
1398         3  removed - files that exist in SRCURI but not in exported files
1399      In each dict the key is the 'basepath' of the URI and value is the
1400      absolute path to the existing file in recipe space (if any).
1401    """
1402    import oe.recipeutils
1403
1404    # Find out local files (SRC_URI files that exist in the "recipe space").
1405    # Local files that reside in srctree are not included in patch generation.
1406    # Instead they are directly copied over the original source files (in
1407    # recipe space).
1408    existing_files = oe.recipeutils.get_recipe_local_files(rd)
1409    new_set = None
1410    updated = OrderedDict()
1411    added = OrderedDict()
1412    removed = OrderedDict()
1413
1414    # Get current branch and return early with empty lists
1415    # if on one of the override branches
1416    # (local files are provided only for the main branch and processing
1417    # them against lists from recipe overrides will result in mismatches
1418    # and broken modifications to recipes).
1419    stdout, _ = bb.process.run('git rev-parse --abbrev-ref HEAD',
1420                               cwd=srctree)
1421    branchname = stdout.rstrip()
1422    if branchname.startswith(override_branch_prefix):
1423        return (updated, added, removed)
1424
1425    local_files_dir = os.path.join(srctreebase, 'oe-local-files')
1426    git_files = _git_ls_tree(srctree)
1427    if 'oe-local-files' in git_files:
1428        # If tracked by Git, take the files from srctree HEAD. First get
1429        # the tree object of the directory
1430        tmp_index = os.path.join(srctree, '.git', 'index.tmp.devtool')
1431        tree = git_files['oe-local-files'][2]
1432        bb.process.run(['git', 'checkout', tree, '--', '.'], cwd=srctree,
1433                        env=dict(os.environ, GIT_WORK_TREE=destdir,
1434                                 GIT_INDEX_FILE=tmp_index))
1435        new_set = list(_git_ls_tree(srctree, tree, True).keys())
1436    elif os.path.isdir(local_files_dir):
1437        # If not tracked by Git, just copy from working copy
1438        new_set = _ls_tree(local_files_dir)
1439        bb.process.run(['cp', '-ax',
1440                        os.path.join(local_files_dir, '.'), destdir])
1441    else:
1442        new_set = []
1443
1444    # Special handling for kernel config
1445    if bb.data.inherits_class('kernel-yocto', rd):
1446        fragment_fn = 'devtool-fragment.cfg'
1447        fragment_path = os.path.join(destdir, fragment_fn)
1448        if _create_kconfig_diff(srctree, rd, fragment_path):
1449            if os.path.exists(fragment_path):
1450                if fragment_fn not in new_set:
1451                    new_set.append(fragment_fn)
1452                # Copy fragment to local-files
1453                if os.path.isdir(local_files_dir):
1454                    shutil.copy2(fragment_path, local_files_dir)
1455            else:
1456                if fragment_fn in new_set:
1457                    new_set.remove(fragment_fn)
1458                # Remove fragment from local-files
1459                if os.path.exists(os.path.join(local_files_dir, fragment_fn)):
1460                    os.unlink(os.path.join(local_files_dir, fragment_fn))
1461
1462    # Special handling for cml1, ccmake, etc bbclasses that generated
1463    # configuration fragment files that are consumed as source files
1464    for frag_class, frag_name in [("cml1", "fragment.cfg"), ("ccmake", "site-file.cmake")]:
1465        if bb.data.inherits_class(frag_class, rd):
1466            srcpath = os.path.join(rd.getVar('WORKDIR'), frag_name)
1467            if os.path.exists(srcpath):
1468                if frag_name not in new_set:
1469                    new_set.append(frag_name)
1470                # copy fragment into destdir
1471                shutil.copy2(srcpath, destdir)
1472                # copy fragment into local files if exists
1473                if os.path.isdir(local_files_dir):
1474                    shutil.copy2(srcpath, local_files_dir)
1475
1476    if new_set is not None:
1477        for fname in new_set:
1478            if fname in existing_files:
1479                origpath = existing_files.pop(fname)
1480                workpath = os.path.join(local_files_dir, fname)
1481                if not filecmp.cmp(origpath, workpath):
1482                    updated[fname] = origpath
1483            elif fname != '.gitignore':
1484                added[fname] = None
1485
1486        workdir = rd.getVar('WORKDIR')
1487        s = rd.getVar('S')
1488        if not s.endswith(os.sep):
1489            s += os.sep
1490
1491        if workdir != s:
1492            # Handle files where subdir= was specified
1493            for fname in list(existing_files.keys()):
1494                # FIXME handle both subdir starting with BP and not?
1495                fworkpath = os.path.join(workdir, fname)
1496                if fworkpath.startswith(s):
1497                    fpath = os.path.join(srctree, os.path.relpath(fworkpath, s))
1498                    if os.path.exists(fpath):
1499                        origpath = existing_files.pop(fname)
1500                        if not filecmp.cmp(origpath, fpath):
1501                            updated[fpath] = origpath
1502
1503        removed = existing_files
1504    return (updated, added, removed)
1505
1506
1507def _determine_files_dir(rd):
1508    """Determine the appropriate files directory for a recipe"""
1509    recipedir = rd.getVar('FILE_DIRNAME')
1510    for entry in rd.getVar('FILESPATH').split(':'):
1511        relpth = os.path.relpath(entry, recipedir)
1512        if not os.sep in relpth:
1513            # One (or zero) levels below only, so we don't put anything in machine-specific directories
1514            if os.path.isdir(entry):
1515                return entry
1516    return os.path.join(recipedir, rd.getVar('BPN'))
1517
1518
1519def _update_recipe_srcrev(recipename, workspace, srctree, rd, appendlayerdir, wildcard_version, no_remove, no_report_remove, dry_run_outdir=None):
1520    """Implement the 'srcrev' mode of update-recipe"""
1521    import bb
1522    import oe.recipeutils
1523
1524    dry_run_suffix = ' (dry-run)' if dry_run_outdir else ''
1525
1526    recipefile = rd.getVar('FILE')
1527    recipedir = os.path.basename(recipefile)
1528    logger.info('Updating SRCREV in recipe %s%s' % (recipedir, dry_run_suffix))
1529
1530    # Get HEAD revision
1531    try:
1532        stdout, _ = bb.process.run('git rev-parse HEAD', cwd=srctree)
1533    except bb.process.ExecutionError as err:
1534        raise DevtoolError('Failed to get HEAD revision in %s: %s' %
1535                           (srctree, err))
1536    srcrev = stdout.strip()
1537    if len(srcrev) != 40:
1538        raise DevtoolError('Invalid hash returned by git: %s' % stdout)
1539
1540    destpath = None
1541    remove_files = []
1542    patchfields = {}
1543    patchfields['SRCREV'] = srcrev
1544    orig_src_uri = rd.getVar('SRC_URI', False) or ''
1545    srcuri = orig_src_uri.split()
1546    tempdir = tempfile.mkdtemp(prefix='devtool')
1547    update_srcuri = False
1548    appendfile = None
1549    try:
1550        local_files_dir = tempfile.mkdtemp(dir=tempdir)
1551        srctreebase = workspace[recipename]['srctreebase']
1552        upd_f, new_f, del_f = _export_local_files(srctree, rd, local_files_dir, srctreebase)
1553        if not no_remove:
1554            # Find list of existing patches in recipe file
1555            patches_dir = tempfile.mkdtemp(dir=tempdir)
1556            old_srcrev = rd.getVar('SRCREV') or ''
1557            upd_p, new_p, del_p = _export_patches(srctree, rd, old_srcrev,
1558                                                  patches_dir)
1559            logger.debug('Patches: update %s, new %s, delete %s' % (dict(upd_p), dict(new_p), dict(del_p)))
1560
1561            # Remove deleted local files and "overlapping" patches
1562            remove_files = list(del_f.values()) + list(upd_p.values()) + list(del_p.values())
1563            if remove_files:
1564                removedentries = _remove_file_entries(srcuri, remove_files)[0]
1565                update_srcuri = True
1566
1567        if appendlayerdir:
1568            files = dict((os.path.join(local_files_dir, key), val) for
1569                          key, val in list(upd_f.items()) + list(new_f.items()))
1570            removevalues = {}
1571            if update_srcuri:
1572                removevalues  = {'SRC_URI': removedentries}
1573                patchfields['SRC_URI'] = '\\\n    '.join(srcuri)
1574            if dry_run_outdir:
1575                logger.info('Creating bbappend (dry-run)')
1576            else:
1577                appendfile, destpath = oe.recipeutils.bbappend_recipe(
1578                        rd, appendlayerdir, files, wildcardver=wildcard_version,
1579                        extralines=patchfields, removevalues=removevalues,
1580                        redirect_output=dry_run_outdir)
1581        else:
1582            files_dir = _determine_files_dir(rd)
1583            for basepath, path in upd_f.items():
1584                logger.info('Updating file %s%s' % (basepath, dry_run_suffix))
1585                if os.path.isabs(basepath):
1586                    # Original file (probably with subdir pointing inside source tree)
1587                    # so we do not want to move it, just copy
1588                    _copy_file(basepath, path, dry_run_outdir=dry_run_outdir, base_outdir=recipedir)
1589                else:
1590                    _move_file(os.path.join(local_files_dir, basepath), path,
1591                               dry_run_outdir=dry_run_outdir, base_outdir=recipedir)
1592                update_srcuri= True
1593            for basepath, path in new_f.items():
1594                logger.info('Adding new file %s%s' % (basepath, dry_run_suffix))
1595                _move_file(os.path.join(local_files_dir, basepath),
1596                           os.path.join(files_dir, basepath),
1597                           dry_run_outdir=dry_run_outdir,
1598                           base_outdir=recipedir)
1599                srcuri.append('file://%s' % basepath)
1600                update_srcuri = True
1601            if update_srcuri:
1602                patchfields['SRC_URI'] = ' '.join(srcuri)
1603            ret = oe.recipeutils.patch_recipe(rd, recipefile, patchfields, redirect_output=dry_run_outdir)
1604    finally:
1605        shutil.rmtree(tempdir)
1606    if not 'git://' in orig_src_uri:
1607        logger.info('You will need to update SRC_URI within the recipe to '
1608                    'point to a git repository where you have pushed your '
1609                    'changes')
1610
1611    _remove_source_files(appendlayerdir, remove_files, destpath, no_report_remove, dry_run=dry_run_outdir)
1612    return True, appendfile, remove_files
1613
1614def _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):
1615    """Implement the 'patch' mode of update-recipe"""
1616    import bb
1617    import oe.recipeutils
1618
1619    recipefile = rd.getVar('FILE')
1620    recipedir = os.path.dirname(recipefile)
1621    append = workspace[recipename]['bbappend']
1622    if not os.path.exists(append):
1623        raise DevtoolError('unable to find workspace bbappend for recipe %s' %
1624                           recipename)
1625    srctreebase = workspace[recipename]['srctreebase']
1626    relpatchdir = os.path.relpath(srctreebase, srctree)
1627    if relpatchdir == '.':
1628        patchdir_params = {}
1629    else:
1630        patchdir_params = {'patchdir': relpatchdir}
1631
1632    def srcuri_entry(basepath):
1633        if patchdir_params:
1634            paramstr = ';' + ';'.join('%s=%s' % (k,v) for k,v in patchdir_params.items())
1635        else:
1636            paramstr = ''
1637        return 'file://%s%s' % (basepath, paramstr)
1638
1639    initial_rev, update_rev, changed_revs, filter_patches = _get_patchset_revs(srctree, append, initial_rev, force_patch_refresh)
1640    if not initial_rev:
1641        raise DevtoolError('Unable to find initial revision - please specify '
1642                           'it with --initial-rev')
1643
1644    appendfile = None
1645    dl_dir = rd.getVar('DL_DIR')
1646    if not dl_dir.endswith('/'):
1647        dl_dir += '/'
1648
1649    dry_run_suffix = ' (dry-run)' if dry_run_outdir else ''
1650
1651    tempdir = tempfile.mkdtemp(prefix='devtool')
1652    try:
1653        local_files_dir = tempfile.mkdtemp(dir=tempdir)
1654        upd_f, new_f, del_f = _export_local_files(srctree, rd, local_files_dir, srctreebase)
1655
1656        # Get updated patches from source tree
1657        patches_dir = tempfile.mkdtemp(dir=tempdir)
1658        upd_p, new_p, _ = _export_patches(srctree, rd, update_rev,
1659                                          patches_dir, changed_revs)
1660        # Get all patches from source tree and check if any should be removed
1661        all_patches_dir = tempfile.mkdtemp(dir=tempdir)
1662        _, _, del_p = _export_patches(srctree, rd, initial_rev,
1663                                      all_patches_dir)
1664        logger.debug('Pre-filtering: update: %s, new: %s' % (dict(upd_p), dict(new_p)))
1665        if filter_patches:
1666            new_p = OrderedDict()
1667            upd_p = OrderedDict((k,v) for k,v in upd_p.items() if k in filter_patches)
1668            del_p = OrderedDict((k,v) for k,v in del_p.items() if k in filter_patches)
1669        remove_files = []
1670        if not no_remove:
1671            # Remove deleted local files and  patches
1672            remove_files = list(del_f.values()) + list(del_p.values())
1673        updatefiles = False
1674        updaterecipe = False
1675        destpath = None
1676        srcuri = (rd.getVar('SRC_URI', False) or '').split()
1677        if appendlayerdir:
1678            files = OrderedDict((os.path.join(local_files_dir, key), val) for
1679                         key, val in list(upd_f.items()) + list(new_f.items()))
1680            files.update(OrderedDict((os.path.join(patches_dir, key), val) for
1681                              key, val in list(upd_p.items()) + list(new_p.items())))
1682            if files or remove_files:
1683                removevalues = None
1684                if remove_files:
1685                    removedentries, remaining = _remove_file_entries(
1686                                                    srcuri, remove_files)
1687                    if removedentries or remaining:
1688                        remaining = [srcuri_entry(os.path.basename(item)) for
1689                                     item in remaining]
1690                        removevalues = {'SRC_URI': removedentries + remaining}
1691                appendfile, destpath = oe.recipeutils.bbappend_recipe(
1692                                rd, appendlayerdir, files,
1693                                wildcardver=wildcard_version,
1694                                removevalues=removevalues,
1695                                redirect_output=dry_run_outdir,
1696                                params=[patchdir_params] * len(files))
1697            else:
1698                logger.info('No patches or local source files needed updating')
1699        else:
1700            # Update existing files
1701            files_dir = _determine_files_dir(rd)
1702            for basepath, path in upd_f.items():
1703                logger.info('Updating file %s' % basepath)
1704                if os.path.isabs(basepath):
1705                    # Original file (probably with subdir pointing inside source tree)
1706                    # so we do not want to move it, just copy
1707                    _copy_file(basepath, path,
1708                               dry_run_outdir=dry_run_outdir, base_outdir=recipedir)
1709                else:
1710                    _move_file(os.path.join(local_files_dir, basepath), path,
1711                               dry_run_outdir=dry_run_outdir, base_outdir=recipedir)
1712                updatefiles = True
1713            for basepath, path in upd_p.items():
1714                patchfn = os.path.join(patches_dir, basepath)
1715                if os.path.dirname(path) + '/' == dl_dir:
1716                    # This is a a downloaded patch file - we now need to
1717                    # replace the entry in SRC_URI with our local version
1718                    logger.info('Replacing remote patch %s with updated local version' % basepath)
1719                    path = os.path.join(files_dir, basepath)
1720                    _replace_srcuri_entry(srcuri, basepath, srcuri_entry(basepath))
1721                    updaterecipe = True
1722                else:
1723                    logger.info('Updating patch %s%s' % (basepath, dry_run_suffix))
1724                _move_file(patchfn, path,
1725                           dry_run_outdir=dry_run_outdir, base_outdir=recipedir)
1726                updatefiles = True
1727            # Add any new files
1728            for basepath, path in new_f.items():
1729                logger.info('Adding new file %s%s' % (basepath, dry_run_suffix))
1730                _move_file(os.path.join(local_files_dir, basepath),
1731                           os.path.join(files_dir, basepath),
1732                           dry_run_outdir=dry_run_outdir,
1733                           base_outdir=recipedir)
1734                srcuri.append(srcuri_entry(basepath))
1735                updaterecipe = True
1736            for basepath, path in new_p.items():
1737                logger.info('Adding new patch %s%s' % (basepath, dry_run_suffix))
1738                _move_file(os.path.join(patches_dir, basepath),
1739                           os.path.join(files_dir, basepath),
1740                           dry_run_outdir=dry_run_outdir,
1741                           base_outdir=recipedir)
1742                srcuri.append(srcuri_entry(basepath))
1743                updaterecipe = True
1744            # Update recipe, if needed
1745            if _remove_file_entries(srcuri, remove_files)[0]:
1746                updaterecipe = True
1747            if updaterecipe:
1748                if not dry_run_outdir:
1749                    logger.info('Updating recipe %s' % os.path.basename(recipefile))
1750                ret = oe.recipeutils.patch_recipe(rd, recipefile,
1751                                                  {'SRC_URI': ' '.join(srcuri)},
1752                                                  redirect_output=dry_run_outdir)
1753            elif not updatefiles:
1754                # Neither patches nor recipe were updated
1755                logger.info('No patches or files need updating')
1756                return False, None, []
1757    finally:
1758        shutil.rmtree(tempdir)
1759
1760    _remove_source_files(appendlayerdir, remove_files, destpath, no_report_remove, dry_run=dry_run_outdir)
1761    return True, appendfile, remove_files
1762
1763def _guess_recipe_update_mode(srctree, rdata):
1764    """Guess the recipe update mode to use"""
1765    src_uri = (rdata.getVar('SRC_URI') or '').split()
1766    git_uris = [uri for uri in src_uri if uri.startswith('git://')]
1767    if not git_uris:
1768        return 'patch'
1769    # Just use the first URI for now
1770    uri = git_uris[0]
1771    # Check remote branch
1772    params = bb.fetch.decodeurl(uri)[5]
1773    upstr_branch = params['branch'] if 'branch' in params else 'master'
1774    # Check if current branch HEAD is found in upstream branch
1775    stdout, _ = bb.process.run('git rev-parse HEAD', cwd=srctree)
1776    head_rev = stdout.rstrip()
1777    stdout, _ = bb.process.run('git branch -r --contains %s' % head_rev,
1778                               cwd=srctree)
1779    remote_brs = [branch.strip() for branch in stdout.splitlines()]
1780    if 'origin/' + upstr_branch in remote_brs:
1781        return 'srcrev'
1782
1783    return 'patch'
1784
1785def _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):
1786    srctree = workspace[recipename]['srctree']
1787    if mode == 'auto':
1788        mode = _guess_recipe_update_mode(srctree, rd)
1789
1790    override_branches = []
1791    mainbranch = None
1792    startbranch = None
1793    if not no_overrides:
1794        stdout, _ = bb.process.run('git branch', cwd=srctree)
1795        other_branches = []
1796        for line in stdout.splitlines():
1797            branchname = line[2:]
1798            if line.startswith('* '):
1799                startbranch = branchname
1800            if branchname.startswith(override_branch_prefix):
1801                override_branches.append(branchname)
1802            else:
1803                other_branches.append(branchname)
1804
1805        if override_branches:
1806            logger.debug('_update_recipe: override branches: %s' % override_branches)
1807            logger.debug('_update_recipe: other branches: %s' % other_branches)
1808            if startbranch.startswith(override_branch_prefix):
1809                if len(other_branches) == 1:
1810                    mainbranch = other_branches[1]
1811                else:
1812                    raise DevtoolError('Unable to determine main branch - please check out the main branch in source tree first')
1813            else:
1814                mainbranch = startbranch
1815
1816    checkedout = None
1817    anyupdated = False
1818    appendfile = None
1819    allremoved = []
1820    if override_branches:
1821        logger.info('Handling main branch (%s)...' % mainbranch)
1822        if startbranch != mainbranch:
1823            bb.process.run('git checkout %s' % mainbranch, cwd=srctree)
1824        checkedout = mainbranch
1825    try:
1826        branchlist = [mainbranch] + override_branches
1827        for branch in branchlist:
1828            crd = bb.data.createCopy(rd)
1829            if branch != mainbranch:
1830                logger.info('Handling branch %s...' % branch)
1831                override = branch[len(override_branch_prefix):]
1832                crd.appendVar('OVERRIDES', ':%s' % override)
1833                bb.process.run('git checkout %s' % branch, cwd=srctree)
1834                checkedout = branch
1835
1836            if mode == 'srcrev':
1837                updated, appendf, removed = _update_recipe_srcrev(recipename, workspace, srctree, crd, appendlayerdir, wildcard_version, no_remove, no_report_remove, dry_run_outdir)
1838            elif mode == 'patch':
1839                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)
1840            else:
1841                raise DevtoolError('update_recipe: invalid mode %s' % mode)
1842            if updated:
1843                anyupdated = True
1844            if appendf:
1845                appendfile = appendf
1846            allremoved.extend(removed)
1847    finally:
1848        if startbranch and checkedout != startbranch:
1849            bb.process.run('git checkout %s' % startbranch, cwd=srctree)
1850
1851    return anyupdated, appendfile, allremoved
1852
1853def update_recipe(args, config, basepath, workspace):
1854    """Entry point for the devtool 'update-recipe' subcommand"""
1855    check_workspace_recipe(workspace, args.recipename)
1856
1857    if args.append:
1858        if not os.path.exists(args.append):
1859            raise DevtoolError('bbappend destination layer directory "%s" '
1860                               'does not exist' % args.append)
1861        if not os.path.exists(os.path.join(args.append, 'conf', 'layer.conf')):
1862            raise DevtoolError('conf/layer.conf not found in bbappend '
1863                               'destination layer "%s"' % args.append)
1864
1865    tinfoil = setup_tinfoil(basepath=basepath, tracking=True)
1866    try:
1867
1868        rd = parse_recipe(config, tinfoil, args.recipename, True)
1869        if not rd:
1870            return 1
1871
1872        dry_run_output = None
1873        dry_run_outdir = None
1874        if args.dry_run:
1875            dry_run_output = tempfile.TemporaryDirectory(prefix='devtool')
1876            dry_run_outdir = dry_run_output.name
1877        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)
1878
1879        if updated:
1880            rf = rd.getVar('FILE')
1881            if rf.startswith(config.workspace_path):
1882                logger.warning('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)
1883    finally:
1884        tinfoil.shutdown()
1885
1886    return 0
1887
1888
1889def status(args, config, basepath, workspace):
1890    """Entry point for the devtool 'status' subcommand"""
1891    if workspace:
1892        for recipe, value in sorted(workspace.items()):
1893            recipefile = value['recipefile']
1894            if recipefile:
1895                recipestr = ' (%s)' % recipefile
1896            else:
1897                recipestr = ''
1898            print("%s: %s%s" % (recipe, value['srctree'], recipestr))
1899    else:
1900        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')
1901    return 0
1902
1903
1904def _reset(recipes, no_clean, remove_work, config, basepath, workspace):
1905    """Reset one or more recipes"""
1906    import oe.path
1907
1908    def clean_preferred_provider(pn, layerconf_path):
1909        """Remove PREFERRED_PROVIDER from layer.conf'"""
1910        import re
1911        layerconf_file = os.path.join(layerconf_path, 'conf', 'layer.conf')
1912        new_layerconf_file = os.path.join(layerconf_path, 'conf', '.layer.conf')
1913        pprovider_found = False
1914        with open(layerconf_file, 'r') as f:
1915            lines = f.readlines()
1916            with open(new_layerconf_file, 'a') as nf:
1917                for line in lines:
1918                    pprovider_exp = r'^PREFERRED_PROVIDER_.*? = "' + pn + r'"$'
1919                    if not re.match(pprovider_exp, line):
1920                        nf.write(line)
1921                    else:
1922                        pprovider_found = True
1923        if pprovider_found:
1924            shutil.move(new_layerconf_file, layerconf_file)
1925        else:
1926            os.remove(new_layerconf_file)
1927
1928    if recipes and not no_clean:
1929        if len(recipes) == 1:
1930            logger.info('Cleaning sysroot for recipe %s...' % recipes[0])
1931        else:
1932            logger.info('Cleaning sysroot for recipes %s...' % ', '.join(recipes))
1933        # If the recipe file itself was created in the workspace, and
1934        # it uses BBCLASSEXTEND, then we need to also clean the other
1935        # variants
1936        targets = []
1937        for recipe in recipes:
1938            targets.append(recipe)
1939            recipefile = workspace[recipe]['recipefile']
1940            if recipefile and os.path.exists(recipefile):
1941                targets.extend(get_bbclassextend_targets(recipefile, recipe))
1942        try:
1943            exec_build_env_command(config.init_path, basepath, 'bitbake -c clean %s' % ' '.join(targets))
1944        except bb.process.ExecutionError as e:
1945            raise DevtoolError('Command \'%s\' failed, output:\n%s\nIf you '
1946                                'wish, you may specify -n/--no-clean to '
1947                                'skip running this command when resetting' %
1948                                (e.command, e.stdout))
1949
1950    for pn in recipes:
1951        _check_preserve(config, pn)
1952
1953        appendfile = workspace[pn]['bbappend']
1954        if os.path.exists(appendfile):
1955            # This shouldn't happen, but is possible if devtool errored out prior to
1956            # writing the md5 file. We need to delete this here or the recipe won't
1957            # actually be reset
1958            os.remove(appendfile)
1959
1960        preservepath = os.path.join(config.workspace_path, 'attic', pn, pn)
1961        def preservedir(origdir):
1962            if os.path.exists(origdir):
1963                for root, dirs, files in os.walk(origdir):
1964                    for fn in files:
1965                        logger.warning('Preserving %s in %s' % (fn, preservepath))
1966                        _move_file(os.path.join(origdir, fn),
1967                                   os.path.join(preservepath, fn))
1968                    for dn in dirs:
1969                        preservedir(os.path.join(root, dn))
1970                os.rmdir(origdir)
1971
1972        recipefile = workspace[pn]['recipefile']
1973        if recipefile and oe.path.is_path_parent(config.workspace_path, recipefile):
1974            # This should always be true if recipefile is set, but just in case
1975            preservedir(os.path.dirname(recipefile))
1976        # We don't automatically create this dir next to appends, but the user can
1977        preservedir(os.path.join(config.workspace_path, 'appends', pn))
1978
1979        srctreebase = workspace[pn]['srctreebase']
1980        if os.path.isdir(srctreebase):
1981            if os.listdir(srctreebase):
1982                    if remove_work:
1983                        logger.info('-r argument used on %s, removing source tree.'
1984                                    ' You will lose any unsaved work' %pn)
1985                        shutil.rmtree(srctreebase)
1986                    else:
1987                        # We don't want to risk wiping out any work in progress
1988                        if srctreebase.startswith(os.path.join(config.workspace_path, 'sources')):
1989                            from datetime import datetime
1990                            preservesrc = os.path.join(config.workspace_path, 'attic', 'sources', "{}.{}".format(pn,datetime.now().strftime("%Y%m%d%H%M%S")))
1991                            logger.info('Preserving source tree in %s\nIf you no '
1992                                        'longer need it then please delete it manually.\n'
1993                                        'It is also possible to reuse it via devtool source tree argument.'
1994                                        % preservesrc)
1995                            bb.utils.mkdirhier(os.path.dirname(preservesrc))
1996                            shutil.move(srctreebase, preservesrc)
1997                        else:
1998                            logger.info('Leaving source tree %s as-is; if you no '
1999                                        'longer need it then please delete it manually'
2000                                        % srctreebase)
2001            else:
2002                # This is unlikely, but if it's empty we can just remove it
2003                os.rmdir(srctreebase)
2004
2005        clean_preferred_provider(pn, config.workspace_path)
2006
2007def reset(args, config, basepath, workspace):
2008    """Entry point for the devtool 'reset' subcommand"""
2009    import bb
2010    import shutil
2011
2012    recipes = ""
2013
2014    if args.recipename:
2015        if args.all:
2016            raise DevtoolError("Recipe cannot be specified if -a/--all is used")
2017        else:
2018            for recipe in args.recipename:
2019                check_workspace_recipe(workspace, recipe, checksrc=False)
2020    elif not args.all:
2021        raise DevtoolError("Recipe must be specified, or specify -a/--all to "
2022                           "reset all recipes")
2023    if args.all:
2024        recipes = list(workspace.keys())
2025    else:
2026        recipes = args.recipename
2027
2028    _reset(recipes, args.no_clean, args.remove_work, config, basepath, workspace)
2029
2030    return 0
2031
2032
2033def _get_layer(layername, d):
2034    """Determine the base layer path for the specified layer name/path"""
2035    layerdirs = d.getVar('BBLAYERS').split()
2036    layers = {}    # {basename: layer_paths}
2037    for p in layerdirs:
2038        bn = os.path.basename(p)
2039        if bn not in layers:
2040            layers[bn] = [p]
2041        else:
2042            layers[bn].append(p)
2043    # Provide some shortcuts
2044    if layername.lower() in ['oe-core', 'openembedded-core']:
2045        layername = 'meta'
2046    layer_paths = layers.get(layername, None)
2047    if not layer_paths:
2048        return os.path.abspath(layername)
2049    elif len(layer_paths) == 1:
2050        return os.path.abspath(layer_paths[0])
2051    else:
2052        # multiple layers having the same base name
2053        logger.warning("Multiple layers have the same base name '%s', use the first one '%s'." % (layername, layer_paths[0]))
2054        logger.warning("Consider using path instead of base name to specify layer:\n\t\t%s" % '\n\t\t'.join(layer_paths))
2055        return os.path.abspath(layer_paths[0])
2056
2057
2058def finish(args, config, basepath, workspace):
2059    """Entry point for the devtool 'finish' subcommand"""
2060    import bb
2061    import oe.recipeutils
2062
2063    check_workspace_recipe(workspace, args.recipename)
2064
2065    dry_run_suffix = ' (dry-run)' if args.dry_run else ''
2066
2067    # Grab the equivalent of COREBASE without having to initialise tinfoil
2068    corebasedir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..'))
2069
2070    srctree = workspace[args.recipename]['srctree']
2071    check_git_repo_op(srctree, [corebasedir])
2072    dirty = check_git_repo_dirty(srctree)
2073    if dirty:
2074        if args.force:
2075            logger.warning('Source tree is not clean, continuing as requested by -f/--force')
2076        else:
2077            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)
2078
2079    no_clean = args.no_clean
2080    remove_work=args.remove_work
2081    tinfoil = setup_tinfoil(basepath=basepath, tracking=True)
2082    try:
2083        rd = parse_recipe(config, tinfoil, args.recipename, True)
2084        if not rd:
2085            return 1
2086
2087        destlayerdir = _get_layer(args.destination, tinfoil.config_data)
2088        recipefile = rd.getVar('FILE')
2089        recipedir = os.path.dirname(recipefile)
2090        origlayerdir = oe.recipeutils.find_layerdir(recipefile)
2091
2092        if not os.path.isdir(destlayerdir):
2093            raise DevtoolError('Unable to find layer or directory matching "%s"' % args.destination)
2094
2095        if os.path.abspath(destlayerdir) == config.workspace_path:
2096            raise DevtoolError('"%s" specifies the workspace layer - that is not a valid destination' % args.destination)
2097
2098        # If it's an upgrade, grab the original path
2099        origpath = None
2100        origfilelist = None
2101        append = workspace[args.recipename]['bbappend']
2102        with open(append, 'r') as f:
2103            for line in f:
2104                if line.startswith('# original_path:'):
2105                    origpath = line.split(':')[1].strip()
2106                elif line.startswith('# original_files:'):
2107                    origfilelist = line.split(':')[1].split()
2108
2109        destlayerbasedir = oe.recipeutils.find_layerdir(destlayerdir)
2110
2111        if origlayerdir == config.workspace_path:
2112            # Recipe file itself is in workspace, update it there first
2113            appendlayerdir = None
2114            origrelpath = None
2115            if origpath:
2116                origlayerpath = oe.recipeutils.find_layerdir(origpath)
2117                if origlayerpath:
2118                    origrelpath = os.path.relpath(origpath, origlayerpath)
2119            destpath = oe.recipeutils.get_bbfile_path(rd, destlayerdir, origrelpath)
2120            if not destpath:
2121                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))
2122            # Warn if the layer isn't in bblayers.conf (the code to create a bbappend will do this in other cases)
2123            layerdirs = [os.path.abspath(layerdir) for layerdir in rd.getVar('BBLAYERS').split()]
2124            if not os.path.abspath(destlayerbasedir) in layerdirs:
2125                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)
2126
2127        elif destlayerdir == origlayerdir:
2128            # Same layer, update the original recipe
2129            appendlayerdir = None
2130            destpath = None
2131        else:
2132            # Create/update a bbappend in the specified layer
2133            appendlayerdir = destlayerdir
2134            destpath = None
2135
2136        # Actually update the recipe / bbappend
2137        removing_original = (origpath and origfilelist and oe.recipeutils.find_layerdir(origpath) == destlayerbasedir)
2138        dry_run_output = None
2139        dry_run_outdir = None
2140        if args.dry_run:
2141            dry_run_output = tempfile.TemporaryDirectory(prefix='devtool')
2142            dry_run_outdir = dry_run_output.name
2143        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)
2144        removed = [os.path.relpath(pth, recipedir) for pth in removed]
2145
2146        # Remove any old files in the case of an upgrade
2147        if removing_original:
2148            for fn in origfilelist:
2149                fnp = os.path.join(origpath, fn)
2150                if fn in removed or not os.path.exists(os.path.join(recipedir, fn)):
2151                    logger.info('Removing file %s%s' % (fnp, dry_run_suffix))
2152                if not args.dry_run:
2153                    try:
2154                        os.remove(fnp)
2155                    except FileNotFoundError:
2156                        pass
2157
2158        if origlayerdir == config.workspace_path and destpath:
2159            # Recipe file itself is in the workspace - need to move it and any
2160            # associated files to the specified layer
2161            no_clean = True
2162            logger.info('Moving recipe file to %s%s' % (destpath, dry_run_suffix))
2163            for root, _, files in os.walk(recipedir):
2164                for fn in files:
2165                    srcpath = os.path.join(root, fn)
2166                    relpth = os.path.relpath(os.path.dirname(srcpath), recipedir)
2167                    destdir = os.path.abspath(os.path.join(destpath, relpth))
2168                    destfp = os.path.join(destdir, fn)
2169                    _move_file(srcpath, destfp, dry_run_outdir=dry_run_outdir, base_outdir=destpath)
2170
2171        if dry_run_outdir:
2172            import difflib
2173            comparelist = []
2174            for root, _, files in os.walk(dry_run_outdir):
2175                for fn in files:
2176                    outf = os.path.join(root, fn)
2177                    relf = os.path.relpath(outf, dry_run_outdir)
2178                    logger.debug('dry-run: output file %s' % relf)
2179                    if fn.endswith('.bb'):
2180                        if origfilelist and origpath and destpath:
2181                            # Need to match this up with the pre-upgrade recipe file
2182                            for origf in origfilelist:
2183                                if origf.endswith('.bb'):
2184                                    comparelist.append((os.path.abspath(os.path.join(origpath, origf)),
2185                                                        outf,
2186                                                        os.path.abspath(os.path.join(destpath, relf))))
2187                                    break
2188                        else:
2189                            # Compare to the existing recipe
2190                            comparelist.append((recipefile, outf, recipefile))
2191                    elif fn.endswith('.bbappend'):
2192                        if appendfile:
2193                            if os.path.exists(appendfile):
2194                                comparelist.append((appendfile, outf, appendfile))
2195                            else:
2196                                comparelist.append((None, outf, appendfile))
2197                    else:
2198                        if destpath:
2199                            recipedest = destpath
2200                        elif appendfile:
2201                            recipedest = os.path.dirname(appendfile)
2202                        else:
2203                            recipedest = os.path.dirname(recipefile)
2204                        destfp = os.path.join(recipedest, relf)
2205                        if os.path.exists(destfp):
2206                            comparelist.append((destfp, outf, destfp))
2207            output = ''
2208            for oldfile, newfile, newfileshow in comparelist:
2209                if oldfile:
2210                    with open(oldfile, 'r') as f:
2211                        oldlines = f.readlines()
2212                else:
2213                    oldfile = '/dev/null'
2214                    oldlines = []
2215                with open(newfile, 'r') as f:
2216                    newlines = f.readlines()
2217                if not newfileshow:
2218                    newfileshow = newfile
2219                diff = difflib.unified_diff(oldlines, newlines, oldfile, newfileshow)
2220                difflines = list(diff)
2221                if difflines:
2222                    output += ''.join(difflines)
2223            if output:
2224                logger.info('Diff of changed files:\n%s' % output)
2225    finally:
2226        tinfoil.shutdown()
2227
2228    # Everything else has succeeded, we can now reset
2229    if args.dry_run:
2230        logger.info('Resetting recipe (dry-run)')
2231    else:
2232        _reset([args.recipename], no_clean=no_clean, remove_work=remove_work, config=config, basepath=basepath, workspace=workspace)
2233
2234    return 0
2235
2236
2237def get_default_srctree(config, recipename=''):
2238    """Get the default srctree path"""
2239    srctreeparent = config.get('General', 'default_source_parent_dir', config.workspace_path)
2240    if recipename:
2241        return os.path.join(srctreeparent, 'sources', recipename)
2242    else:
2243        return os.path.join(srctreeparent, 'sources')
2244
2245def register_commands(subparsers, context):
2246    """Register devtool subcommands from this plugin"""
2247
2248    defsrctree = get_default_srctree(context.config)
2249    parser_add = subparsers.add_parser('add', help='Add a new recipe',
2250                                       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.',
2251                                       group='starting', order=100)
2252    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.')
2253    parser_add.add_argument('srctree', nargs='?', help='Path to external source tree. If not specified, a subdirectory of %s will be used.' % defsrctree)
2254    parser_add.add_argument('fetchuri', nargs='?', help='Fetch the specified URI and extract it to create the source tree')
2255    group = parser_add.add_mutually_exclusive_group()
2256    group.add_argument('--same-dir', '-s', help='Build in same directory as source', action="store_true")
2257    group.add_argument('--no-same-dir', help='Force build in a separate build directory', action="store_true")
2258    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')
2259    parser_add.add_argument('--npm-dev', help='For npm, also fetch devDependencies', action="store_true")
2260    parser_add.add_argument('--version', '-V', help='Version to use within recipe (PV)')
2261    parser_add.add_argument('--no-git', '-g', help='If fetching source, do not set up source tree as a git repository', action="store_true")
2262    group = parser_add.add_mutually_exclusive_group()
2263    group.add_argument('--srcrev', '-S', help='Source revision to fetch if fetching from an SCM such as git (default latest)')
2264    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")
2265    parser_add.add_argument('--srcbranch', '-B', help='Branch in source repository if fetching from an SCM such as git (default master)')
2266    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')
2267    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')
2268    parser_add.add_argument('--src-subdir', help='Specify subdirectory within source tree to use', metavar='SUBDIR')
2269    parser_add.add_argument('--mirrors', help='Enable PREMIRRORS and MIRRORS for source tree fetching (disable by default).', action="store_true")
2270    parser_add.add_argument('--provides', '-p', help='Specify an alias for the item provided by the recipe. E.g. virtual/libgl')
2271    parser_add.set_defaults(func=add, fixed_setup=context.fixed_setup)
2272
2273    parser_modify = subparsers.add_parser('modify', help='Modify the source for an existing recipe',
2274                                       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.',
2275                                       group='starting', order=90)
2276    parser_modify.add_argument('recipename', help='Name of existing recipe to edit (just name - no version, path or extension)')
2277    parser_modify.add_argument('srctree', nargs='?', help='Path to external source tree. If not specified, a subdirectory of %s will be used.' % defsrctree)
2278    parser_modify.add_argument('--wildcard', '-w', action="store_true", help='Use wildcard for unversioned bbappend')
2279    group = parser_modify.add_mutually_exclusive_group()
2280    group.add_argument('--extract', '-x', action="store_true", help='Extract source for recipe (default)')
2281    group.add_argument('--no-extract', '-n', action="store_true", help='Do not extract source, expect it to exist')
2282    group = parser_modify.add_mutually_exclusive_group()
2283    group.add_argument('--same-dir', '-s', help='Build in same directory as source', action="store_true")
2284    group.add_argument('--no-same-dir', help='Force build in a separate build directory', action="store_true")
2285    parser_modify.add_argument('--branch', '-b', default="devtool", help='Name for development branch to checkout (when not using -n/--no-extract) (default "%(default)s")')
2286    parser_modify.add_argument('--no-overrides', '-O', action="store_true", help='Do not create branches for other override configurations')
2287    parser_modify.add_argument('--keep-temp', help='Keep temporary directory (for debugging)', action="store_true")
2288    parser_modify.set_defaults(func=modify, fixed_setup=context.fixed_setup)
2289
2290    parser_extract = subparsers.add_parser('extract', help='Extract the source for an existing recipe',
2291                                       description='Extracts the source for an existing recipe',
2292                                       group='advanced')
2293    parser_extract.add_argument('recipename', help='Name of recipe to extract the source for')
2294    parser_extract.add_argument('srctree', help='Path to where to extract the source tree')
2295    parser_extract.add_argument('--branch', '-b', default="devtool", help='Name for development branch to checkout (default "%(default)s")')
2296    parser_extract.add_argument('--no-overrides', '-O', action="store_true", help='Do not create branches for other override configurations')
2297    parser_extract.add_argument('--keep-temp', action="store_true", help='Keep temporary directory (for debugging)')
2298    parser_extract.set_defaults(func=extract, fixed_setup=context.fixed_setup)
2299
2300    parser_sync = subparsers.add_parser('sync', help='Synchronize the source tree for an existing recipe',
2301                                       description='Synchronize the previously extracted source tree for an existing recipe',
2302                                       formatter_class=argparse.ArgumentDefaultsHelpFormatter,
2303                                       group='advanced')
2304    parser_sync.add_argument('recipename', help='Name of recipe to sync the source for')
2305    parser_sync.add_argument('srctree', help='Path to the source tree')
2306    parser_sync.add_argument('--branch', '-b', default="devtool", help='Name for development branch to checkout')
2307    parser_sync.add_argument('--keep-temp', action="store_true", help='Keep temporary directory (for debugging)')
2308    parser_sync.set_defaults(func=sync, fixed_setup=context.fixed_setup)
2309
2310    parser_rename = subparsers.add_parser('rename', help='Rename a recipe file in the workspace',
2311                                       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.',
2312                                       group='working', order=10)
2313    parser_rename.add_argument('recipename', help='Current name of recipe to rename')
2314    parser_rename.add_argument('newname', nargs='?', help='New name for recipe (optional, not needed if you only want to change the version)')
2315    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)')
2316    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')
2317    parser_rename.set_defaults(func=rename)
2318
2319    parser_update_recipe = subparsers.add_parser('update-recipe', help='Apply changes from external source tree to recipe',
2320                                       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.',
2321                                       group='working', order=-90)
2322    parser_update_recipe.add_argument('recipename', help='Name of recipe to update')
2323    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')
2324    parser_update_recipe.add_argument('--initial-rev', help='Override starting revision for patches')
2325    parser_update_recipe.add_argument('--append', '-a', help='Write changes to a bbappend in the specified layer instead of the recipe', metavar='LAYERDIR')
2326    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')
2327    parser_update_recipe.add_argument('--no-remove', '-n', action="store_true", help='Don\'t remove patches, only add or update')
2328    parser_update_recipe.add_argument('--no-overrides', '-O', action="store_true", help='Do not handle other override branches (if they exist)')
2329    parser_update_recipe.add_argument('--dry-run', '-N', action="store_true", help='Dry-run (just report changes instead of writing them)')
2330    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)')
2331    parser_update_recipe.set_defaults(func=update_recipe)
2332
2333    parser_status = subparsers.add_parser('status', help='Show workspace status',
2334                                          description='Lists recipes currently in your workspace and the paths to their respective external source trees',
2335                                          group='info', order=100)
2336    parser_status.set_defaults(func=status)
2337
2338    parser_reset = subparsers.add_parser('reset', help='Remove a recipe from your workspace',
2339                                         description='Removes the specified recipe(s) from your workspace (resetting its state back to that defined by the metadata).',
2340                                         group='working', order=-100)
2341    parser_reset.add_argument('recipename', nargs='*', help='Recipe to reset')
2342    parser_reset.add_argument('--all', '-a', action="store_true", help='Reset all recipes (clear workspace)')
2343    parser_reset.add_argument('--no-clean', '-n', action="store_true", help='Don\'t clean the sysroot to remove recipe output')
2344    parser_reset.add_argument('--remove-work', '-r', action="store_true", help='Clean the sources directory along with append')
2345    parser_reset.set_defaults(func=reset)
2346
2347    parser_finish = subparsers.add_parser('finish', help='Finish working on a recipe in your workspace',
2348                                         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.',
2349                                         group='working', order=-100)
2350    parser_finish.add_argument('recipename', help='Recipe to finish')
2351    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.')
2352    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')
2353    parser_finish.add_argument('--initial-rev', help='Override starting revision for patches')
2354    parser_finish.add_argument('--force', '-f', action="store_true", help='Force continuing even if there are uncommitted changes in the source tree repository')
2355    parser_finish.add_argument('--remove-work', '-r', action="store_true", help='Clean the sources directory under workspace')
2356    parser_finish.add_argument('--no-clean', '-n', action="store_true", help='Don\'t clean the sysroot to remove recipe output')
2357    parser_finish.add_argument('--no-overrides', '-O', action="store_true", help='Do not handle other override branches (if they exist)')
2358    parser_finish.add_argument('--dry-run', '-N', action="store_true", help='Dry-run (just report changes instead of writing them)')
2359    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)')
2360    parser_finish.set_defaults(func=finish)
2361