xref: /OK3568_Linux_fs/yocto/poky/scripts/lib/devtool/standard.py (revision 4882a59341e53eb6f0b4789bf948001014eff981)
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('DEVTOOL_TEMPDIR = "%s"\n' % tempdir)
571            f.write('DEVTOOL_DEVBRANCH = "%s"\n' % devbranch)
572            if not is_kernel_yocto:
573                f.write('PATCHTOOL = "git"\n')
574                f.write('PATCH_COMMIT_FUNCTIONS = "1"\n')
575            if extra_overrides:
576                f.write('DEVTOOL_EXTRA_OVERRIDES = "%s"\n' % ':'.join(extra_overrides))
577            f.write('inherit devtool-source\n')
578            f.write('###--- _extract_source\n')
579
580        update_unlockedsigs(basepath, workspace, fixed_setup, [pn])
581
582        sstate_manifests = d.getVar('SSTATE_MANIFESTS')
583        bb.utils.mkdirhier(sstate_manifests)
584        preservestampfile = os.path.join(sstate_manifests, 'preserve-stamps')
585        with open(preservestampfile, 'w') as f:
586            f.write(d.getVar('STAMP'))
587        try:
588            if is_kernel_yocto:
589                # We need to generate the kernel config
590                task = 'do_configure'
591            else:
592                task = 'do_patch'
593
594                if 'noexec' in (d.getVarFlags(task, False) or []) or 'task' not in (d.getVarFlags(task, False) or []):
595                    logger.info('The %s recipe has %s disabled. Running only '
596                                       'do_configure task dependencies' % (pn, task))
597
598                    if 'depends' in d.getVarFlags('do_configure', False):
599                        pn = d.getVarFlags('do_configure', False)['depends']
600                        pn = pn.replace('${PV}', d.getVar('PV'))
601                        pn = pn.replace('${COMPILERDEP}', d.getVar('COMPILERDEP'))
602                        task = None
603
604            # Run the fetch + unpack tasks
605            res = tinfoil.build_targets(pn,
606                                        task,
607                                        handle_events=True)
608        finally:
609            if os.path.exists(preservestampfile):
610                os.remove(preservestampfile)
611
612        if not res:
613            raise DevtoolError('Extracting source for %s failed' % pn)
614
615        if not is_kernel_yocto and ('noexec' in (d.getVarFlags('do_patch', False) or []) or 'task' not in (d.getVarFlags('do_patch', False) or [])):
616            workshareddir = d.getVar('S')
617            if os.path.islink(srctree):
618                os.unlink(srctree)
619
620            os.symlink(workshareddir, srctree)
621
622            # The initial_rev file is created in devtool_post_unpack function that will not be executed if
623            # do_unpack/do_patch tasks are disabled so we have to directly say that source extraction was successful
624            return True, True
625
626        try:
627            with open(os.path.join(tempdir, 'initial_rev'), 'r') as f:
628                initial_rev = f.read()
629
630            with open(os.path.join(tempdir, 'srcsubdir'), 'r') as f:
631                srcsubdir = f.read()
632        except FileNotFoundError as e:
633            raise DevtoolError('Something went wrong with source extraction - the devtool-source class was not active or did not function correctly:\n%s' % str(e))
634        srcsubdir_rel = os.path.relpath(srcsubdir, os.path.join(tempdir, 'workdir'))
635
636        # Check if work-shared is empty, if yes
637        # find source and copy to work-shared
638        if is_kernel_yocto:
639            workshareddir = d.getVar('STAGING_KERNEL_DIR')
640            staging_kerVer = get_staging_kver(workshareddir)
641            kernelVersion = d.getVar('LINUX_VERSION')
642
643            # handle dangling symbolic link in work-shared:
644            if os.path.islink(workshareddir):
645                os.unlink(workshareddir)
646
647            if os.path.exists(workshareddir) and (not os.listdir(workshareddir) or kernelVersion != staging_kerVer):
648                shutil.rmtree(workshareddir)
649                oe.path.copyhardlinktree(srcsubdir,workshareddir)
650            elif not os.path.exists(workshareddir):
651                oe.path.copyhardlinktree(srcsubdir,workshareddir)
652
653        tempdir_localdir = os.path.join(tempdir, 'oe-local-files')
654        srctree_localdir = os.path.join(srctree, 'oe-local-files')
655
656        if sync:
657            bb.process.run('git fetch file://' + srcsubdir + ' ' + devbranch + ':' + devbranch, cwd=srctree)
658
659            # Move oe-local-files directory to srctree
660            # As the oe-local-files is not part of the constructed git tree,
661            # remove them directly during the synchrounizating might surprise
662            # the users.  Instead, we move it to oe-local-files.bak and remind
663            # user in the log message.
664            if os.path.exists(srctree_localdir + '.bak'):
665                shutil.rmtree(srctree_localdir, srctree_localdir + '.bak')
666
667            if os.path.exists(srctree_localdir):
668                logger.info('Backing up current local file directory %s' % srctree_localdir)
669                shutil.move(srctree_localdir, srctree_localdir + '.bak')
670
671            if os.path.exists(tempdir_localdir):
672                logger.info('Syncing local source files to srctree...')
673                shutil.copytree(tempdir_localdir, srctree_localdir)
674        else:
675            # Move oe-local-files directory to srctree
676            if os.path.exists(tempdir_localdir):
677                logger.info('Adding local source files to srctree...')
678                shutil.move(tempdir_localdir, srcsubdir)
679
680            shutil.move(srcsubdir, srctree)
681            symlink_oelocal_files_srctree(d,srctree)
682
683        if is_kernel_yocto:
684            logger.info('Copying kernel config to srctree')
685            shutil.copy2(os.path.join(tempdir, '.config'), srctree)
686
687    finally:
688        if appendbackup:
689            shutil.copyfile(appendbackup, appendfile)
690        elif os.path.exists(appendfile):
691            os.remove(appendfile)
692        if keep_temp:
693            logger.info('Preserving temporary directory %s' % tempdir)
694        else:
695            shutil.rmtree(tempdir)
696    return initial_rev, srcsubdir_rel
697
698def _add_md5(config, recipename, filename):
699    """Record checksum of a file (or recursively for a directory) to the md5-file of the workspace"""
700    import bb.utils
701
702    def addfile(fn):
703        md5 = bb.utils.md5_file(fn)
704        with open(os.path.join(config.workspace_path, '.devtool_md5'), 'a+') as f:
705            md5_str = '%s|%s|%s\n' % (recipename, os.path.relpath(fn, config.workspace_path), md5)
706            f.seek(0, os.SEEK_SET)
707            if not md5_str in f.read():
708                f.write(md5_str)
709
710    if os.path.isdir(filename):
711        for root, _, files in os.walk(filename):
712            for f in files:
713                addfile(os.path.join(root, f))
714    else:
715        addfile(filename)
716
717def _check_preserve(config, recipename):
718    """Check if a file was manually changed and needs to be saved in 'attic'
719       directory"""
720    import bb.utils
721    origfile = os.path.join(config.workspace_path, '.devtool_md5')
722    newfile = os.path.join(config.workspace_path, '.devtool_md5_new')
723    preservepath = os.path.join(config.workspace_path, 'attic', recipename)
724    with open(origfile, 'r') as f:
725        with open(newfile, 'w') as tf:
726            for line in f.readlines():
727                splitline = line.rstrip().split('|')
728                if splitline[0] == recipename:
729                    removefile = os.path.join(config.workspace_path, splitline[1])
730                    try:
731                        md5 = bb.utils.md5_file(removefile)
732                    except IOError as err:
733                        if err.errno == 2:
734                            # File no longer exists, skip it
735                            continue
736                        else:
737                            raise
738                    if splitline[2] != md5:
739                        bb.utils.mkdirhier(preservepath)
740                        preservefile = os.path.basename(removefile)
741                        logger.warning('File %s modified since it was written, preserving in %s' % (preservefile, preservepath))
742                        shutil.move(removefile, os.path.join(preservepath, preservefile))
743                    else:
744                        os.remove(removefile)
745                else:
746                    tf.write(line)
747    bb.utils.rename(newfile, origfile)
748
749def get_staging_kver(srcdir):
750    # Kernel version from work-shared
751    kerver = []
752    staging_kerVer=""
753    if os.path.exists(srcdir) and os.listdir(srcdir):
754        with open(os.path.join(srcdir,"Makefile")) as f:
755            version = [next(f) for x in range(5)][1:4]
756            for word in version:
757                kerver.append(word.split('= ')[1].split('\n')[0])
758            staging_kerVer = ".".join(kerver)
759    return staging_kerVer
760
761def get_staging_kbranch(srcdir):
762    staging_kbranch = ""
763    if os.path.exists(srcdir) and os.listdir(srcdir):
764        (branch, _) = bb.process.run('git branch | grep \* | cut -d \' \' -f2', cwd=srcdir)
765        staging_kbranch = "".join(branch.split('\n')[0])
766    return staging_kbranch
767
768def get_real_srctree(srctree, s, workdir):
769    # Check that recipe isn't using a shared workdir
770    s = os.path.abspath(s)
771    workdir = os.path.abspath(workdir)
772    if s.startswith(workdir) and s != workdir and os.path.dirname(s) != workdir:
773        # Handle if S is set to a subdirectory of the source
774        srcsubdir = os.path.relpath(s, workdir).split(os.sep, 1)[1]
775        srctree = os.path.join(srctree, srcsubdir)
776    return srctree
777
778def modify(args, config, basepath, workspace):
779    """Entry point for the devtool 'modify' subcommand"""
780    import bb
781    import oe.recipeutils
782    import oe.patch
783    import oe.path
784
785    if args.recipename in workspace:
786        raise DevtoolError("recipe %s is already in your workspace" %
787                           args.recipename)
788
789    tinfoil = setup_tinfoil(basepath=basepath, tracking=True)
790    try:
791        rd = parse_recipe(config, tinfoil, args.recipename, True)
792        if not rd:
793            return 1
794
795        pn = rd.getVar('PN')
796        if pn != args.recipename:
797            logger.info('Mapping %s to %s' % (args.recipename, pn))
798        if pn in workspace:
799            raise DevtoolError("recipe %s is already in your workspace" %
800                            pn)
801
802        if args.srctree:
803            srctree = os.path.abspath(args.srctree)
804        else:
805            srctree = get_default_srctree(config, pn)
806
807        if args.no_extract and not os.path.isdir(srctree):
808            raise DevtoolError("--no-extract specified and source path %s does "
809                            "not exist or is not a directory" %
810                            srctree)
811
812        recipefile = rd.getVar('FILE')
813        appendfile = recipe_to_append(recipefile, config, args.wildcard)
814        if os.path.exists(appendfile):
815            raise DevtoolError("Another variant of recipe %s is already in your "
816                            "workspace (only one variant of a recipe can "
817                            "currently be worked on at once)"
818                            % pn)
819
820        _check_compatible_recipe(pn, rd)
821
822        initial_rev = None
823        commits = []
824        check_commits = False
825
826        if bb.data.inherits_class('kernel-yocto', rd):
827            # Current set kernel version
828            kernelVersion = rd.getVar('LINUX_VERSION')
829            srcdir = rd.getVar('STAGING_KERNEL_DIR')
830            kbranch = rd.getVar('KBRANCH')
831
832            staging_kerVer = get_staging_kver(srcdir)
833            staging_kbranch = get_staging_kbranch(srcdir)
834            if (os.path.exists(srcdir) and os.listdir(srcdir)) and (kernelVersion in staging_kerVer and staging_kbranch == kbranch):
835                oe.path.copyhardlinktree(srcdir,srctree)
836                workdir = rd.getVar('WORKDIR')
837                srcsubdir = rd.getVar('S')
838                localfilesdir = os.path.join(srctree,'oe-local-files')
839                # Move local source files into separate subdir
840                recipe_patches = [os.path.basename(patch) for patch in oe.recipeutils.get_recipe_patches(rd)]
841                local_files = oe.recipeutils.get_recipe_local_files(rd)
842
843                for key in local_files.copy():
844                    if key.endswith('scc'):
845                        sccfile = open(local_files[key], 'r')
846                        for l in sccfile:
847                            line = l.split()
848                            if line and line[0] in ('kconf', 'patch'):
849                                cfg = os.path.join(os.path.dirname(local_files[key]), line[-1])
850                                if not cfg in local_files.values():
851                                    local_files[line[-1]] = cfg
852                                    shutil.copy2(cfg, workdir)
853                        sccfile.close()
854
855                # Ignore local files with subdir={BP}
856                srcabspath = os.path.abspath(srcsubdir)
857                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))]
858                if local_files:
859                    for fname in local_files:
860                        _move_file(os.path.join(workdir, fname), os.path.join(srctree, 'oe-local-files', fname))
861                    with open(os.path.join(srctree, 'oe-local-files', '.gitignore'), 'w') as f:
862                        f.write('# Ignore local files, by default. Remove this file ''if you want to commit the directory to Git\n*\n')
863
864                symlink_oelocal_files_srctree(rd,srctree)
865
866                task = 'do_configure'
867                res = tinfoil.build_targets(pn, task, handle_events=True)
868
869                # Copy .config to workspace
870                kconfpath = rd.getVar('B')
871                logger.info('Copying kernel config to workspace')
872                shutil.copy2(os.path.join(kconfpath, '.config'),srctree)
873
874                # Set this to true, we still need to get initial_rev
875                # by parsing the git repo
876                args.no_extract = True
877
878        if not args.no_extract:
879            initial_rev, _ = _extract_source(srctree, args.keep_temp, args.branch, False, config, basepath, workspace, args.fixed_setup, rd, tinfoil, no_overrides=args.no_overrides)
880            if not initial_rev:
881                return 1
882            logger.info('Source tree extracted to %s' % srctree)
883            if os.path.exists(os.path.join(srctree, '.git')):
884                # Get list of commits since this revision
885                (stdout, _) = bb.process.run('git rev-list --reverse %s..HEAD' % initial_rev, cwd=srctree)
886                commits = stdout.split()
887                check_commits = True
888        else:
889            if os.path.exists(os.path.join(srctree, '.git')):
890                # Check if it's a tree previously extracted by us. This is done
891                # by ensuring that devtool-base and args.branch (devtool) exist.
892                # The check_commits logic will cause an exception if either one
893                # of these doesn't exist
894                try:
895                    (stdout, _) = bb.process.run('git branch --contains devtool-base', cwd=srctree)
896                    bb.process.run('git rev-parse %s' % args.branch, cwd=srctree)
897                except bb.process.ExecutionError:
898                    stdout = ''
899                if stdout:
900                    check_commits = True
901                for line in stdout.splitlines():
902                    if line.startswith('*'):
903                        (stdout, _) = bb.process.run('git rev-parse devtool-base', cwd=srctree)
904                        initial_rev = stdout.rstrip()
905                if not initial_rev:
906                    # Otherwise, just grab the head revision
907                    (stdout, _) = bb.process.run('git rev-parse HEAD', cwd=srctree)
908                    initial_rev = stdout.rstrip()
909
910        branch_patches = {}
911        if check_commits:
912            # Check if there are override branches
913            (stdout, _) = bb.process.run('git branch', cwd=srctree)
914            branches = []
915            for line in stdout.rstrip().splitlines():
916                branchname = line[2:].rstrip()
917                if branchname.startswith(override_branch_prefix):
918                    branches.append(branchname)
919            if branches:
920                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))
921            branches.insert(0, args.branch)
922            seen_patches = []
923            for branch in branches:
924                branch_patches[branch] = []
925                (stdout, _) = bb.process.run('git log devtool-base..%s' % branch, cwd=srctree)
926                for line in stdout.splitlines():
927                    line = line.strip()
928                    if line.startswith(oe.patch.GitApplyTree.patch_line_prefix):
929                        origpatch = line[len(oe.patch.GitApplyTree.patch_line_prefix):].split(':', 1)[-1].strip()
930                        if not origpatch in seen_patches:
931                            seen_patches.append(origpatch)
932                            branch_patches[branch].append(origpatch)
933
934        # Need to grab this here in case the source is within a subdirectory
935        srctreebase = srctree
936        srctree = get_real_srctree(srctree, rd.getVar('S'), rd.getVar('WORKDIR'))
937
938        bb.utils.mkdirhier(os.path.dirname(appendfile))
939        with open(appendfile, 'w') as f:
940            f.write('FILESEXTRAPATHS:prepend := "${THISDIR}/${PN}:"\n')
941            # Local files can be modified/tracked in separate subdir under srctree
942            # Mostly useful for packages with S != WORKDIR
943            f.write('FILESPATH:prepend := "%s:"\n' %
944                    os.path.join(srctreebase, 'oe-local-files'))
945            f.write('# srctreebase: %s\n' % srctreebase)
946
947            f.write('\ninherit externalsrc\n')
948            f.write('# NOTE: We use pn- overrides here to avoid affecting multiple variants in the case where the recipe uses BBCLASSEXTEND\n')
949            f.write('EXTERNALSRC:pn-%s = "%s"\n' % (pn, srctree))
950
951            b_is_s = use_external_build(args.same_dir, args.no_same_dir, rd)
952            if b_is_s:
953                f.write('EXTERNALSRC_BUILD:pn-%s = "%s"\n' % (pn, srctree))
954
955            if bb.data.inherits_class('kernel', rd):
956                f.write('SRCTREECOVEREDTASKS = "do_validate_branches do_kernel_checkout '
957                        'do_fetch do_unpack do_kernel_configcheck"\n')
958                f.write('\ndo_patch[noexec] = "1"\n')
959                f.write('\ndo_configure:append() {\n'
960                        '    cp ${B}/.config ${S}/.config.baseline\n'
961                        '    ln -sfT ${B}/.config ${S}/.config.new\n'
962                        '}\n')
963                f.write('\ndo_kernel_configme:prepend() {\n'
964                        '    if [ -e ${S}/.config ]; then\n'
965                        '        mv ${S}/.config ${S}/.config.old\n'
966                        '    fi\n'
967                        '}\n')
968            if rd.getVarFlag('do_menuconfig','task'):
969                f.write('\ndo_configure:append() {\n'
970                '    if [ ! ${DEVTOOL_DISABLE_MENUCONFIG} ]; then\n'
971                '        cp ${B}/.config ${S}/.config.baseline\n'
972                '        ln -sfT ${B}/.config ${S}/.config.new\n'
973                '    fi\n'
974                '}\n')
975            if initial_rev:
976                f.write('\n# initial_rev: %s\n' % initial_rev)
977                for commit in commits:
978                    f.write('# commit: %s\n' % commit)
979            if branch_patches:
980                for branch in branch_patches:
981                    if branch == args.branch:
982                        continue
983                    f.write('# patches_%s: %s\n' % (branch, ','.join(branch_patches[branch])))
984
985        update_unlockedsigs(basepath, workspace, args.fixed_setup, [pn])
986
987        _add_md5(config, pn, appendfile)
988
989        logger.info('Recipe %s now set up to build from %s' % (pn, srctree))
990
991    finally:
992        tinfoil.shutdown()
993
994    return 0
995
996
997def rename(args, config, basepath, workspace):
998    """Entry point for the devtool 'rename' subcommand"""
999    import bb
1000    import oe.recipeutils
1001
1002    check_workspace_recipe(workspace, args.recipename)
1003
1004    if not (args.newname or args.version):
1005        raise DevtoolError('You must specify a new name, a version with -V/--version, or both')
1006
1007    recipefile = workspace[args.recipename]['recipefile']
1008    if not recipefile:
1009        raise DevtoolError('devtool rename can only be used where the recipe file itself is in the workspace (e.g. after devtool add)')
1010
1011    if args.newname and args.newname != args.recipename:
1012        reason = oe.recipeutils.validate_pn(args.newname)
1013        if reason:
1014            raise DevtoolError(reason)
1015        newname = args.newname
1016    else:
1017        newname = args.recipename
1018
1019    append = workspace[args.recipename]['bbappend']
1020    appendfn = os.path.splitext(os.path.basename(append))[0]
1021    splitfn = appendfn.split('_')
1022    if len(splitfn) > 1:
1023        origfnver = appendfn.split('_')[1]
1024    else:
1025        origfnver = ''
1026
1027    recipefilemd5 = None
1028    tinfoil = setup_tinfoil(basepath=basepath, tracking=True)
1029    try:
1030        rd = parse_recipe(config, tinfoil, args.recipename, True)
1031        if not rd:
1032            return 1
1033
1034        bp = rd.getVar('BP')
1035        bpn = rd.getVar('BPN')
1036        if newname != args.recipename:
1037            localdata = rd.createCopy()
1038            localdata.setVar('PN', newname)
1039            newbpn = localdata.getVar('BPN')
1040        else:
1041            newbpn = bpn
1042        s = rd.getVar('S', False)
1043        src_uri = rd.getVar('SRC_URI', False)
1044        pv = rd.getVar('PV')
1045
1046        # Correct variable values that refer to the upstream source - these
1047        # values must stay the same, so if the name/version are changing then
1048        # we need to fix them up
1049        new_s = s
1050        new_src_uri = src_uri
1051        if newbpn != bpn:
1052            # ${PN} here is technically almost always incorrect, but people do use it
1053            new_s = new_s.replace('${BPN}', bpn)
1054            new_s = new_s.replace('${PN}', bpn)
1055            new_s = new_s.replace('${BP}', '%s-${PV}' % bpn)
1056            new_src_uri = new_src_uri.replace('${BPN}', bpn)
1057            new_src_uri = new_src_uri.replace('${PN}', bpn)
1058            new_src_uri = new_src_uri.replace('${BP}', '%s-${PV}' % bpn)
1059        if args.version and origfnver == pv:
1060            new_s = new_s.replace('${PV}', pv)
1061            new_s = new_s.replace('${BP}', '${BPN}-%s' % pv)
1062            new_src_uri = new_src_uri.replace('${PV}', pv)
1063            new_src_uri = new_src_uri.replace('${BP}', '${BPN}-%s' % pv)
1064        patchfields = {}
1065        if new_s != s:
1066            patchfields['S'] = new_s
1067        if new_src_uri != src_uri:
1068            patchfields['SRC_URI'] = new_src_uri
1069        if patchfields:
1070            recipefilemd5 = bb.utils.md5_file(recipefile)
1071            oe.recipeutils.patch_recipe(rd, recipefile, patchfields)
1072            newrecipefilemd5 = bb.utils.md5_file(recipefile)
1073    finally:
1074        tinfoil.shutdown()
1075
1076    if args.version:
1077        newver = args.version
1078    else:
1079        newver = origfnver
1080
1081    if newver:
1082        newappend = '%s_%s.bbappend' % (newname, newver)
1083        newfile =  '%s_%s.bb' % (newname, newver)
1084    else:
1085        newappend = '%s.bbappend' % newname
1086        newfile = '%s.bb' % newname
1087
1088    oldrecipedir = os.path.dirname(recipefile)
1089    newrecipedir = os.path.join(config.workspace_path, 'recipes', newname)
1090    if oldrecipedir != newrecipedir:
1091        bb.utils.mkdirhier(newrecipedir)
1092
1093    newappend = os.path.join(os.path.dirname(append), newappend)
1094    newfile = os.path.join(newrecipedir, newfile)
1095
1096    # Rename bbappend
1097    logger.info('Renaming %s to %s' % (append, newappend))
1098    bb.utils.rename(append, newappend)
1099    # Rename recipe file
1100    logger.info('Renaming %s to %s' % (recipefile, newfile))
1101    bb.utils.rename(recipefile, newfile)
1102
1103    # Rename source tree if it's the default path
1104    appendmd5 = None
1105    if not args.no_srctree:
1106        srctree = workspace[args.recipename]['srctree']
1107        if os.path.abspath(srctree) == os.path.join(config.workspace_path, 'sources', args.recipename):
1108            newsrctree = os.path.join(config.workspace_path, 'sources', newname)
1109            logger.info('Renaming %s to %s' % (srctree, newsrctree))
1110            shutil.move(srctree, newsrctree)
1111            # Correct any references (basically EXTERNALSRC*) in the .bbappend
1112            appendmd5 = bb.utils.md5_file(newappend)
1113            appendlines = []
1114            with open(newappend, 'r') as f:
1115                for line in f:
1116                    appendlines.append(line)
1117            with open(newappend, 'w') as f:
1118                for line in appendlines:
1119                    if srctree in line:
1120                        line = line.replace(srctree, newsrctree)
1121                    f.write(line)
1122            newappendmd5 = bb.utils.md5_file(newappend)
1123
1124    bpndir = None
1125    newbpndir = None
1126    if newbpn != bpn:
1127        bpndir = os.path.join(oldrecipedir, bpn)
1128        if os.path.exists(bpndir):
1129            newbpndir = os.path.join(newrecipedir, newbpn)
1130            logger.info('Renaming %s to %s' % (bpndir, newbpndir))
1131            shutil.move(bpndir, newbpndir)
1132
1133    bpdir = None
1134    newbpdir = None
1135    if newver != origfnver or newbpn != bpn:
1136        bpdir = os.path.join(oldrecipedir, bp)
1137        if os.path.exists(bpdir):
1138            newbpdir = os.path.join(newrecipedir, '%s-%s' % (newbpn, newver))
1139            logger.info('Renaming %s to %s' % (bpdir, newbpdir))
1140            shutil.move(bpdir, newbpdir)
1141
1142    if oldrecipedir != newrecipedir:
1143        # Move any stray files and delete the old recipe directory
1144        for entry in os.listdir(oldrecipedir):
1145            oldpath = os.path.join(oldrecipedir, entry)
1146            newpath = os.path.join(newrecipedir, entry)
1147            logger.info('Renaming %s to %s' % (oldpath, newpath))
1148            shutil.move(oldpath, newpath)
1149        os.rmdir(oldrecipedir)
1150
1151    # Now take care of entries in .devtool_md5
1152    md5entries = []
1153    with open(os.path.join(config.workspace_path, '.devtool_md5'), 'r') as f:
1154        for line in f:
1155            md5entries.append(line)
1156
1157    if bpndir and newbpndir:
1158        relbpndir = os.path.relpath(bpndir, config.workspace_path) + '/'
1159    else:
1160        relbpndir = None
1161    if bpdir and newbpdir:
1162        relbpdir = os.path.relpath(bpdir, config.workspace_path) + '/'
1163    else:
1164        relbpdir = None
1165
1166    with open(os.path.join(config.workspace_path, '.devtool_md5'), 'w') as f:
1167        for entry in md5entries:
1168            splitentry = entry.rstrip().split('|')
1169            if len(splitentry) > 2:
1170                if splitentry[0] == args.recipename:
1171                    splitentry[0] = newname
1172                    if splitentry[1] == os.path.relpath(append, config.workspace_path):
1173                        splitentry[1] = os.path.relpath(newappend, config.workspace_path)
1174                        if appendmd5 and splitentry[2] == appendmd5:
1175                            splitentry[2] = newappendmd5
1176                    elif splitentry[1] == os.path.relpath(recipefile, config.workspace_path):
1177                        splitentry[1] = os.path.relpath(newfile, config.workspace_path)
1178                        if recipefilemd5 and splitentry[2] == recipefilemd5:
1179                            splitentry[2] = newrecipefilemd5
1180                    elif relbpndir and splitentry[1].startswith(relbpndir):
1181                        splitentry[1] = os.path.relpath(os.path.join(newbpndir, splitentry[1][len(relbpndir):]), config.workspace_path)
1182                    elif relbpdir and splitentry[1].startswith(relbpdir):
1183                        splitentry[1] = os.path.relpath(os.path.join(newbpdir, splitentry[1][len(relbpdir):]), config.workspace_path)
1184                    entry = '|'.join(splitentry) + '\n'
1185            f.write(entry)
1186    return 0
1187
1188
1189def _get_patchset_revs(srctree, recipe_path, initial_rev=None, force_patch_refresh=False):
1190    """Get initial and update rev of a recipe. These are the start point of the
1191    whole patchset and start point for the patches to be re-generated/updated.
1192    """
1193    import bb
1194
1195    # Get current branch
1196    stdout, _ = bb.process.run('git rev-parse --abbrev-ref HEAD',
1197                               cwd=srctree)
1198    branchname = stdout.rstrip()
1199
1200    # Parse initial rev from recipe if not specified
1201    commits = []
1202    patches = []
1203    with open(recipe_path, 'r') as f:
1204        for line in f:
1205            if line.startswith('# initial_rev:'):
1206                if not initial_rev:
1207                    initial_rev = line.split(':')[-1].strip()
1208            elif line.startswith('# commit:') and not force_patch_refresh:
1209                commits.append(line.split(':')[-1].strip())
1210            elif line.startswith('# patches_%s:' % branchname):
1211                patches = line.split(':')[-1].strip().split(',')
1212
1213    update_rev = initial_rev
1214    changed_revs = None
1215    if initial_rev:
1216        # Find first actually changed revision
1217        stdout, _ = bb.process.run('git rev-list --reverse %s..HEAD' %
1218                                   initial_rev, cwd=srctree)
1219        newcommits = stdout.split()
1220        for i in range(min(len(commits), len(newcommits))):
1221            if newcommits[i] == commits[i]:
1222                update_rev = commits[i]
1223
1224        try:
1225            stdout, _ = bb.process.run('git cherry devtool-patched',
1226                                        cwd=srctree)
1227        except bb.process.ExecutionError as err:
1228            stdout = None
1229
1230        if stdout is not None and not force_patch_refresh:
1231            changed_revs = []
1232            for line in stdout.splitlines():
1233                if line.startswith('+ '):
1234                    rev = line.split()[1]
1235                    if rev in newcommits:
1236                        changed_revs.append(rev)
1237
1238    return initial_rev, update_rev, changed_revs, patches
1239
1240def _remove_file_entries(srcuri, filelist):
1241    """Remove file:// entries from SRC_URI"""
1242    remaining = filelist[:]
1243    entries = []
1244    for fname in filelist:
1245        basename = os.path.basename(fname)
1246        for i in range(len(srcuri)):
1247            if (srcuri[i].startswith('file://') and
1248                    os.path.basename(srcuri[i].split(';')[0]) == basename):
1249                entries.append(srcuri[i])
1250                remaining.remove(fname)
1251                srcuri.pop(i)
1252                break
1253    return entries, remaining
1254
1255def _replace_srcuri_entry(srcuri, filename, newentry):
1256    """Replace entry corresponding to specified file with a new entry"""
1257    basename = os.path.basename(filename)
1258    for i in range(len(srcuri)):
1259        if os.path.basename(srcuri[i].split(';')[0]) == basename:
1260            srcuri.pop(i)
1261            srcuri.insert(i, newentry)
1262            break
1263
1264def _remove_source_files(append, files, destpath, no_report_remove=False, dry_run=False):
1265    """Unlink existing patch files"""
1266
1267    dry_run_suffix = ' (dry-run)' if dry_run else ''
1268
1269    for path in files:
1270        if append:
1271            if not destpath:
1272                raise Exception('destpath should be set here')
1273            path = os.path.join(destpath, os.path.basename(path))
1274
1275        if os.path.exists(path):
1276            if not no_report_remove:
1277                logger.info('Removing file %s%s' % (path, dry_run_suffix))
1278            if not dry_run:
1279                # FIXME "git rm" here would be nice if the file in question is
1280                #       tracked
1281                # FIXME there's a chance that this file is referred to by
1282                #       another recipe, in which case deleting wouldn't be the
1283                #       right thing to do
1284                os.remove(path)
1285                # Remove directory if empty
1286                try:
1287                    os.rmdir(os.path.dirname(path))
1288                except OSError as ose:
1289                    if ose.errno != errno.ENOTEMPTY:
1290                        raise
1291
1292
1293def _export_patches(srctree, rd, start_rev, destdir, changed_revs=None):
1294    """Export patches from srctree to given location.
1295       Returns three-tuple of dicts:
1296         1. updated - patches that already exist in SRCURI
1297         2. added - new patches that don't exist in SRCURI
1298         3  removed - patches that exist in SRCURI but not in exported patches
1299      In each dict the key is the 'basepath' of the URI and value is the
1300      absolute path to the existing file in recipe space (if any).
1301    """
1302    import oe.recipeutils
1303    from oe.patch import GitApplyTree
1304    updated = OrderedDict()
1305    added = OrderedDict()
1306    seqpatch_re = re.compile('^([0-9]{4}-)?(.+)')
1307
1308    existing_patches = dict((os.path.basename(path), path) for path in
1309                            oe.recipeutils.get_recipe_patches(rd))
1310    logger.debug('Existing patches: %s' % existing_patches)
1311
1312    # Generate patches from Git, exclude local files directory
1313    patch_pathspec = _git_exclude_path(srctree, 'oe-local-files')
1314    GitApplyTree.extractPatches(srctree, start_rev, destdir, patch_pathspec)
1315
1316    new_patches = sorted(os.listdir(destdir))
1317    for new_patch in new_patches:
1318        # Strip numbering from patch names. If it's a git sequence named patch,
1319        # the numbers might not match up since we are starting from a different
1320        # revision This does assume that people are using unique shortlog
1321        # values, but they ought to be anyway...
1322        new_basename = seqpatch_re.match(new_patch).group(2)
1323        match_name = None
1324        for old_patch in existing_patches:
1325            old_basename = seqpatch_re.match(old_patch).group(2)
1326            old_basename_splitext = os.path.splitext(old_basename)
1327            if old_basename.endswith(('.gz', '.bz2', '.Z')) and old_basename_splitext[0] == new_basename:
1328                old_patch_noext = os.path.splitext(old_patch)[0]
1329                match_name = old_patch_noext
1330                break
1331            elif new_basename == old_basename:
1332                match_name = old_patch
1333                break
1334        if match_name:
1335            # Rename patch files
1336            if new_patch != match_name:
1337                bb.utils.rename(os.path.join(destdir, new_patch),
1338                          os.path.join(destdir, match_name))
1339            # Need to pop it off the list now before checking changed_revs
1340            oldpath = existing_patches.pop(old_patch)
1341            if changed_revs is not None:
1342                # Avoid updating patches that have not actually changed
1343                with open(os.path.join(destdir, match_name), 'r') as f:
1344                    firstlineitems = f.readline().split()
1345                    # Looking for "From <hash>" line
1346                    if len(firstlineitems) > 1 and len(firstlineitems[1]) == 40:
1347                        if not firstlineitems[1] in changed_revs:
1348                            continue
1349            # Recompress if necessary
1350            if oldpath.endswith(('.gz', '.Z')):
1351                bb.process.run(['gzip', match_name], cwd=destdir)
1352                if oldpath.endswith('.gz'):
1353                    match_name += '.gz'
1354                else:
1355                    match_name += '.Z'
1356            elif oldpath.endswith('.bz2'):
1357                bb.process.run(['bzip2', match_name], cwd=destdir)
1358                match_name += '.bz2'
1359            updated[match_name] = oldpath
1360        else:
1361            added[new_patch] = None
1362    return (updated, added, existing_patches)
1363
1364
1365def _create_kconfig_diff(srctree, rd, outfile):
1366    """Create a kconfig fragment"""
1367    # Only update config fragment if both config files exist
1368    orig_config = os.path.join(srctree, '.config.baseline')
1369    new_config = os.path.join(srctree, '.config.new')
1370    if os.path.exists(orig_config) and os.path.exists(new_config):
1371        cmd = ['diff', '--new-line-format=%L', '--old-line-format=',
1372               '--unchanged-line-format=', orig_config, new_config]
1373        pipe = subprocess.Popen(cmd, stdout=subprocess.PIPE,
1374                                stderr=subprocess.PIPE)
1375        stdout, stderr = pipe.communicate()
1376        if pipe.returncode == 1:
1377            logger.info("Updating config fragment %s" % outfile)
1378            with open(outfile, 'wb') as fobj:
1379                fobj.write(stdout)
1380        elif pipe.returncode == 0:
1381            logger.info("Would remove config fragment %s" % outfile)
1382            if os.path.exists(outfile):
1383                # Remove fragment file in case of empty diff
1384                logger.info("Removing config fragment %s" % outfile)
1385                os.unlink(outfile)
1386        else:
1387            raise bb.process.ExecutionError(cmd, pipe.returncode, stdout, stderr)
1388        return True
1389    return False
1390
1391
1392def _export_local_files(srctree, rd, destdir, srctreebase):
1393    """Copy local files from srctree to given location.
1394       Returns three-tuple of dicts:
1395         1. updated - files that already exist in SRCURI
1396         2. added - new files files that don't exist in SRCURI
1397         3  removed - files that exist in SRCURI but not in exported files
1398      In each dict the key is the 'basepath' of the URI and value is the
1399      absolute path to the existing file in recipe space (if any).
1400    """
1401    import oe.recipeutils
1402
1403    # Find out local files (SRC_URI files that exist in the "recipe space").
1404    # Local files that reside in srctree are not included in patch generation.
1405    # Instead they are directly copied over the original source files (in
1406    # recipe space).
1407    existing_files = oe.recipeutils.get_recipe_local_files(rd)
1408    new_set = None
1409    updated = OrderedDict()
1410    added = OrderedDict()
1411    removed = OrderedDict()
1412
1413    # Get current branch and return early with empty lists
1414    # if on one of the override branches
1415    # (local files are provided only for the main branch and processing
1416    # them against lists from recipe overrides will result in mismatches
1417    # and broken modifications to recipes).
1418    stdout, _ = bb.process.run('git rev-parse --abbrev-ref HEAD',
1419                               cwd=srctree)
1420    branchname = stdout.rstrip()
1421    if branchname.startswith(override_branch_prefix):
1422        return (updated, added, removed)
1423
1424    local_files_dir = os.path.join(srctreebase, 'oe-local-files')
1425    git_files = _git_ls_tree(srctree)
1426    if 'oe-local-files' in git_files:
1427        # If tracked by Git, take the files from srctree HEAD. First get
1428        # the tree object of the directory
1429        tmp_index = os.path.join(srctree, '.git', 'index.tmp.devtool')
1430        tree = git_files['oe-local-files'][2]
1431        bb.process.run(['git', 'checkout', tree, '--', '.'], cwd=srctree,
1432                        env=dict(os.environ, GIT_WORK_TREE=destdir,
1433                                 GIT_INDEX_FILE=tmp_index))
1434        new_set = list(_git_ls_tree(srctree, tree, True).keys())
1435    elif os.path.isdir(local_files_dir):
1436        # If not tracked by Git, just copy from working copy
1437        new_set = _ls_tree(local_files_dir)
1438        bb.process.run(['cp', '-ax',
1439                        os.path.join(local_files_dir, '.'), destdir])
1440    else:
1441        new_set = []
1442
1443    # Special handling for kernel config
1444    if bb.data.inherits_class('kernel-yocto', rd):
1445        fragment_fn = 'devtool-fragment.cfg'
1446        fragment_path = os.path.join(destdir, fragment_fn)
1447        if _create_kconfig_diff(srctree, rd, fragment_path):
1448            if os.path.exists(fragment_path):
1449                if fragment_fn not in new_set:
1450                    new_set.append(fragment_fn)
1451                # Copy fragment to local-files
1452                if os.path.isdir(local_files_dir):
1453                    shutil.copy2(fragment_path, local_files_dir)
1454            else:
1455                if fragment_fn in new_set:
1456                    new_set.remove(fragment_fn)
1457                # Remove fragment from local-files
1458                if os.path.exists(os.path.join(local_files_dir, fragment_fn)):
1459                    os.unlink(os.path.join(local_files_dir, fragment_fn))
1460
1461    # Special handling for cml1, ccmake, etc bbclasses that generated
1462    # configuration fragment files that are consumed as source files
1463    for frag_class, frag_name in [("cml1", "fragment.cfg"), ("ccmake", "site-file.cmake")]:
1464        if bb.data.inherits_class(frag_class, rd):
1465            srcpath = os.path.join(rd.getVar('WORKDIR'), frag_name)
1466            if os.path.exists(srcpath):
1467                if frag_name not in new_set:
1468                    new_set.append(frag_name)
1469                # copy fragment into destdir
1470                shutil.copy2(srcpath, destdir)
1471                # copy fragment into local files if exists
1472                if os.path.isdir(local_files_dir):
1473                    shutil.copy2(srcpath, local_files_dir)
1474
1475    if new_set is not None:
1476        for fname in new_set:
1477            if fname in existing_files:
1478                origpath = existing_files.pop(fname)
1479                workpath = os.path.join(local_files_dir, fname)
1480                if not filecmp.cmp(origpath, workpath):
1481                    updated[fname] = origpath
1482            elif fname != '.gitignore':
1483                added[fname] = None
1484
1485        workdir = rd.getVar('WORKDIR')
1486        s = rd.getVar('S')
1487        if not s.endswith(os.sep):
1488            s += os.sep
1489
1490        if workdir != s:
1491            # Handle files where subdir= was specified
1492            for fname in list(existing_files.keys()):
1493                # FIXME handle both subdir starting with BP and not?
1494                fworkpath = os.path.join(workdir, fname)
1495                if fworkpath.startswith(s):
1496                    fpath = os.path.join(srctree, os.path.relpath(fworkpath, s))
1497                    if os.path.exists(fpath):
1498                        origpath = existing_files.pop(fname)
1499                        if not filecmp.cmp(origpath, fpath):
1500                            updated[fpath] = origpath
1501
1502        removed = existing_files
1503    return (updated, added, removed)
1504
1505
1506def _determine_files_dir(rd):
1507    """Determine the appropriate files directory for a recipe"""
1508    recipedir = rd.getVar('FILE_DIRNAME')
1509    for entry in rd.getVar('FILESPATH').split(':'):
1510        relpth = os.path.relpath(entry, recipedir)
1511        if not os.sep in relpth:
1512            # One (or zero) levels below only, so we don't put anything in machine-specific directories
1513            if os.path.isdir(entry):
1514                return entry
1515    return os.path.join(recipedir, rd.getVar('BPN'))
1516
1517
1518def _update_recipe_srcrev(recipename, workspace, srctree, rd, appendlayerdir, wildcard_version, no_remove, no_report_remove, dry_run_outdir=None):
1519    """Implement the 'srcrev' mode of update-recipe"""
1520    import bb
1521    import oe.recipeutils
1522
1523    dry_run_suffix = ' (dry-run)' if dry_run_outdir else ''
1524
1525    recipefile = rd.getVar('FILE')
1526    recipedir = os.path.basename(recipefile)
1527    logger.info('Updating SRCREV in recipe %s%s' % (recipedir, dry_run_suffix))
1528
1529    # Get HEAD revision
1530    try:
1531        stdout, _ = bb.process.run('git rev-parse HEAD', cwd=srctree)
1532    except bb.process.ExecutionError as err:
1533        raise DevtoolError('Failed to get HEAD revision in %s: %s' %
1534                           (srctree, err))
1535    srcrev = stdout.strip()
1536    if len(srcrev) != 40:
1537        raise DevtoolError('Invalid hash returned by git: %s' % stdout)
1538
1539    destpath = None
1540    remove_files = []
1541    patchfields = {}
1542    patchfields['SRCREV'] = srcrev
1543    orig_src_uri = rd.getVar('SRC_URI', False) or ''
1544    srcuri = orig_src_uri.split()
1545    tempdir = tempfile.mkdtemp(prefix='devtool')
1546    update_srcuri = False
1547    appendfile = None
1548    try:
1549        local_files_dir = tempfile.mkdtemp(dir=tempdir)
1550        srctreebase = workspace[recipename]['srctreebase']
1551        upd_f, new_f, del_f = _export_local_files(srctree, rd, local_files_dir, srctreebase)
1552        if not no_remove:
1553            # Find list of existing patches in recipe file
1554            patches_dir = tempfile.mkdtemp(dir=tempdir)
1555            old_srcrev = rd.getVar('SRCREV') or ''
1556            upd_p, new_p, del_p = _export_patches(srctree, rd, old_srcrev,
1557                                                  patches_dir)
1558            logger.debug('Patches: update %s, new %s, delete %s' % (dict(upd_p), dict(new_p), dict(del_p)))
1559
1560            # Remove deleted local files and "overlapping" patches
1561            remove_files = list(del_f.values()) + list(upd_p.values()) + list(del_p.values())
1562            if remove_files:
1563                removedentries = _remove_file_entries(srcuri, remove_files)[0]
1564                update_srcuri = True
1565
1566        if appendlayerdir:
1567            files = dict((os.path.join(local_files_dir, key), val) for
1568                          key, val in list(upd_f.items()) + list(new_f.items()))
1569            removevalues = {}
1570            if update_srcuri:
1571                removevalues  = {'SRC_URI': removedentries}
1572                patchfields['SRC_URI'] = '\\\n    '.join(srcuri)
1573            if dry_run_outdir:
1574                logger.info('Creating bbappend (dry-run)')
1575            else:
1576                appendfile, destpath = oe.recipeutils.bbappend_recipe(
1577                        rd, appendlayerdir, files, wildcardver=wildcard_version,
1578                        extralines=patchfields, removevalues=removevalues,
1579                        redirect_output=dry_run_outdir)
1580        else:
1581            files_dir = _determine_files_dir(rd)
1582            for basepath, path in upd_f.items():
1583                logger.info('Updating file %s%s' % (basepath, dry_run_suffix))
1584                if os.path.isabs(basepath):
1585                    # Original file (probably with subdir pointing inside source tree)
1586                    # so we do not want to move it, just copy
1587                    _copy_file(basepath, path, dry_run_outdir=dry_run_outdir, base_outdir=recipedir)
1588                else:
1589                    _move_file(os.path.join(local_files_dir, basepath), path,
1590                               dry_run_outdir=dry_run_outdir, base_outdir=recipedir)
1591                update_srcuri= True
1592            for basepath, path in new_f.items():
1593                logger.info('Adding new file %s%s' % (basepath, dry_run_suffix))
1594                _move_file(os.path.join(local_files_dir, basepath),
1595                           os.path.join(files_dir, basepath),
1596                           dry_run_outdir=dry_run_outdir,
1597                           base_outdir=recipedir)
1598                srcuri.append('file://%s' % basepath)
1599                update_srcuri = True
1600            if update_srcuri:
1601                patchfields['SRC_URI'] = ' '.join(srcuri)
1602            ret = oe.recipeutils.patch_recipe(rd, recipefile, patchfields, redirect_output=dry_run_outdir)
1603    finally:
1604        shutil.rmtree(tempdir)
1605    if not 'git://' in orig_src_uri:
1606        logger.info('You will need to update SRC_URI within the recipe to '
1607                    'point to a git repository where you have pushed your '
1608                    'changes')
1609
1610    _remove_source_files(appendlayerdir, remove_files, destpath, no_report_remove, dry_run=dry_run_outdir)
1611    return True, appendfile, remove_files
1612
1613def _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):
1614    """Implement the 'patch' mode of update-recipe"""
1615    import bb
1616    import oe.recipeutils
1617
1618    recipefile = rd.getVar('FILE')
1619    recipedir = os.path.dirname(recipefile)
1620    append = workspace[recipename]['bbappend']
1621    if not os.path.exists(append):
1622        raise DevtoolError('unable to find workspace bbappend for recipe %s' %
1623                           recipename)
1624    srctreebase = workspace[recipename]['srctreebase']
1625    relpatchdir = os.path.relpath(srctreebase, srctree)
1626    if relpatchdir == '.':
1627        patchdir_params = {}
1628    else:
1629        patchdir_params = {'patchdir': relpatchdir}
1630
1631    def srcuri_entry(fname):
1632        if patchdir_params:
1633            paramstr = ';' + ';'.join('%s=%s' % (k,v) for k,v in patchdir_params.items())
1634        else:
1635            paramstr = ''
1636        return 'file://%s%s' % (basepath, paramstr)
1637
1638    initial_rev, update_rev, changed_revs, filter_patches = _get_patchset_revs(srctree, append, initial_rev, force_patch_refresh)
1639    if not initial_rev:
1640        raise DevtoolError('Unable to find initial revision - please specify '
1641                           'it with --initial-rev')
1642
1643    appendfile = None
1644    dl_dir = rd.getVar('DL_DIR')
1645    if not dl_dir.endswith('/'):
1646        dl_dir += '/'
1647
1648    dry_run_suffix = ' (dry-run)' if dry_run_outdir else ''
1649
1650    tempdir = tempfile.mkdtemp(prefix='devtool')
1651    try:
1652        local_files_dir = tempfile.mkdtemp(dir=tempdir)
1653        upd_f, new_f, del_f = _export_local_files(srctree, rd, local_files_dir, srctreebase)
1654
1655        # Get updated patches from source tree
1656        patches_dir = tempfile.mkdtemp(dir=tempdir)
1657        upd_p, new_p, _ = _export_patches(srctree, rd, update_rev,
1658                                          patches_dir, changed_revs)
1659        # Get all patches from source tree and check if any should be removed
1660        all_patches_dir = tempfile.mkdtemp(dir=tempdir)
1661        _, _, del_p = _export_patches(srctree, rd, initial_rev,
1662                                      all_patches_dir)
1663        logger.debug('Pre-filtering: update: %s, new: %s' % (dict(upd_p), dict(new_p)))
1664        if filter_patches:
1665            new_p = OrderedDict()
1666            upd_p = OrderedDict((k,v) for k,v in upd_p.items() if k in filter_patches)
1667            del_p = OrderedDict((k,v) for k,v in del_p.items() if k in filter_patches)
1668        remove_files = []
1669        if not no_remove:
1670            # Remove deleted local files and  patches
1671            remove_files = list(del_f.values()) + list(del_p.values())
1672        updatefiles = False
1673        updaterecipe = False
1674        destpath = None
1675        srcuri = (rd.getVar('SRC_URI', False) or '').split()
1676        if appendlayerdir:
1677            files = OrderedDict((os.path.join(local_files_dir, key), val) for
1678                         key, val in list(upd_f.items()) + list(new_f.items()))
1679            files.update(OrderedDict((os.path.join(patches_dir, key), val) for
1680                              key, val in list(upd_p.items()) + list(new_p.items())))
1681            if files or remove_files:
1682                removevalues = None
1683                if remove_files:
1684                    removedentries, remaining = _remove_file_entries(
1685                                                    srcuri, remove_files)
1686                    if removedentries or remaining:
1687                        remaining = [srcuri_entry(os.path.basename(item)) for
1688                                     item in remaining]
1689                        removevalues = {'SRC_URI': removedentries + remaining}
1690                appendfile, destpath = oe.recipeutils.bbappend_recipe(
1691                                rd, appendlayerdir, files,
1692                                wildcardver=wildcard_version,
1693                                removevalues=removevalues,
1694                                redirect_output=dry_run_outdir,
1695                                params=[patchdir_params] * len(files))
1696            else:
1697                logger.info('No patches or local source files needed updating')
1698        else:
1699            # Update existing files
1700            files_dir = _determine_files_dir(rd)
1701            for basepath, path in upd_f.items():
1702                logger.info('Updating file %s' % basepath)
1703                if os.path.isabs(basepath):
1704                    # Original file (probably with subdir pointing inside source tree)
1705                    # so we do not want to move it, just copy
1706                    _copy_file(basepath, path,
1707                               dry_run_outdir=dry_run_outdir, base_outdir=recipedir)
1708                else:
1709                    _move_file(os.path.join(local_files_dir, basepath), path,
1710                               dry_run_outdir=dry_run_outdir, base_outdir=recipedir)
1711                updatefiles = True
1712            for basepath, path in upd_p.items():
1713                patchfn = os.path.join(patches_dir, basepath)
1714                if os.path.dirname(path) + '/' == dl_dir:
1715                    # This is a a downloaded patch file - we now need to
1716                    # replace the entry in SRC_URI with our local version
1717                    logger.info('Replacing remote patch %s with updated local version' % basepath)
1718                    path = os.path.join(files_dir, basepath)
1719                    _replace_srcuri_entry(srcuri, basepath, srcuri_entry(basepath))
1720                    updaterecipe = True
1721                else:
1722                    logger.info('Updating patch %s%s' % (basepath, dry_run_suffix))
1723                _move_file(patchfn, path,
1724                           dry_run_outdir=dry_run_outdir, base_outdir=recipedir)
1725                updatefiles = True
1726            # Add any new files
1727            for basepath, path in new_f.items():
1728                logger.info('Adding new file %s%s' % (basepath, dry_run_suffix))
1729                _move_file(os.path.join(local_files_dir, basepath),
1730                           os.path.join(files_dir, basepath),
1731                           dry_run_outdir=dry_run_outdir,
1732                           base_outdir=recipedir)
1733                srcuri.append(srcuri_entry(basepath))
1734                updaterecipe = True
1735            for basepath, path in new_p.items():
1736                logger.info('Adding new patch %s%s' % (basepath, dry_run_suffix))
1737                _move_file(os.path.join(patches_dir, basepath),
1738                           os.path.join(files_dir, basepath),
1739                           dry_run_outdir=dry_run_outdir,
1740                           base_outdir=recipedir)
1741                srcuri.append(srcuri_entry(basepath))
1742                updaterecipe = True
1743            # Update recipe, if needed
1744            if _remove_file_entries(srcuri, remove_files)[0]:
1745                updaterecipe = True
1746            if updaterecipe:
1747                if not dry_run_outdir:
1748                    logger.info('Updating recipe %s' % os.path.basename(recipefile))
1749                ret = oe.recipeutils.patch_recipe(rd, recipefile,
1750                                                  {'SRC_URI': ' '.join(srcuri)},
1751                                                  redirect_output=dry_run_outdir)
1752            elif not updatefiles:
1753                # Neither patches nor recipe were updated
1754                logger.info('No patches or files need updating')
1755                return False, None, []
1756    finally:
1757        shutil.rmtree(tempdir)
1758
1759    _remove_source_files(appendlayerdir, remove_files, destpath, no_report_remove, dry_run=dry_run_outdir)
1760    return True, appendfile, remove_files
1761
1762def _guess_recipe_update_mode(srctree, rdata):
1763    """Guess the recipe update mode to use"""
1764    src_uri = (rdata.getVar('SRC_URI') or '').split()
1765    git_uris = [uri for uri in src_uri if uri.startswith('git://')]
1766    if not git_uris:
1767        return 'patch'
1768    # Just use the first URI for now
1769    uri = git_uris[0]
1770    # Check remote branch
1771    params = bb.fetch.decodeurl(uri)[5]
1772    upstr_branch = params['branch'] if 'branch' in params else 'master'
1773    # Check if current branch HEAD is found in upstream branch
1774    stdout, _ = bb.process.run('git rev-parse HEAD', cwd=srctree)
1775    head_rev = stdout.rstrip()
1776    stdout, _ = bb.process.run('git branch -r --contains %s' % head_rev,
1777                               cwd=srctree)
1778    remote_brs = [branch.strip() for branch in stdout.splitlines()]
1779    if 'origin/' + upstr_branch in remote_brs:
1780        return 'srcrev'
1781
1782    return 'patch'
1783
1784def _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):
1785    srctree = workspace[recipename]['srctree']
1786    if mode == 'auto':
1787        mode = _guess_recipe_update_mode(srctree, rd)
1788
1789    override_branches = []
1790    mainbranch = None
1791    startbranch = None
1792    if not no_overrides:
1793        stdout, _ = bb.process.run('git branch', cwd=srctree)
1794        other_branches = []
1795        for line in stdout.splitlines():
1796            branchname = line[2:]
1797            if line.startswith('* '):
1798                startbranch = branchname
1799            if branchname.startswith(override_branch_prefix):
1800                override_branches.append(branchname)
1801            else:
1802                other_branches.append(branchname)
1803
1804        if override_branches:
1805            logger.debug('_update_recipe: override branches: %s' % override_branches)
1806            logger.debug('_update_recipe: other branches: %s' % other_branches)
1807            if startbranch.startswith(override_branch_prefix):
1808                if len(other_branches) == 1:
1809                    mainbranch = other_branches[1]
1810                else:
1811                    raise DevtoolError('Unable to determine main branch - please check out the main branch in source tree first')
1812            else:
1813                mainbranch = startbranch
1814
1815    checkedout = None
1816    anyupdated = False
1817    appendfile = None
1818    allremoved = []
1819    if override_branches:
1820        logger.info('Handling main branch (%s)...' % mainbranch)
1821        if startbranch != mainbranch:
1822            bb.process.run('git checkout %s' % mainbranch, cwd=srctree)
1823        checkedout = mainbranch
1824    try:
1825        branchlist = [mainbranch] + override_branches
1826        for branch in branchlist:
1827            crd = bb.data.createCopy(rd)
1828            if branch != mainbranch:
1829                logger.info('Handling branch %s...' % branch)
1830                override = branch[len(override_branch_prefix):]
1831                crd.appendVar('OVERRIDES', ':%s' % override)
1832                bb.process.run('git checkout %s' % branch, cwd=srctree)
1833                checkedout = branch
1834
1835            if mode == 'srcrev':
1836                updated, appendf, removed = _update_recipe_srcrev(recipename, workspace, srctree, crd, appendlayerdir, wildcard_version, no_remove, no_report_remove, dry_run_outdir)
1837            elif mode == 'patch':
1838                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)
1839            else:
1840                raise DevtoolError('update_recipe: invalid mode %s' % mode)
1841            if updated:
1842                anyupdated = True
1843            if appendf:
1844                appendfile = appendf
1845            allremoved.extend(removed)
1846    finally:
1847        if startbranch and checkedout != startbranch:
1848            bb.process.run('git checkout %s' % startbranch, cwd=srctree)
1849
1850    return anyupdated, appendfile, allremoved
1851
1852def update_recipe(args, config, basepath, workspace):
1853    """Entry point for the devtool 'update-recipe' subcommand"""
1854    check_workspace_recipe(workspace, args.recipename)
1855
1856    if args.append:
1857        if not os.path.exists(args.append):
1858            raise DevtoolError('bbappend destination layer directory "%s" '
1859                               'does not exist' % args.append)
1860        if not os.path.exists(os.path.join(args.append, 'conf', 'layer.conf')):
1861            raise DevtoolError('conf/layer.conf not found in bbappend '
1862                               'destination layer "%s"' % args.append)
1863
1864    tinfoil = setup_tinfoil(basepath=basepath, tracking=True)
1865    try:
1866
1867        rd = parse_recipe(config, tinfoil, args.recipename, True)
1868        if not rd:
1869            return 1
1870
1871        dry_run_output = None
1872        dry_run_outdir = None
1873        if args.dry_run:
1874            dry_run_output = tempfile.TemporaryDirectory(prefix='devtool')
1875            dry_run_outdir = dry_run_output.name
1876        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)
1877
1878        if updated:
1879            rf = rd.getVar('FILE')
1880            if rf.startswith(config.workspace_path):
1881                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)
1882    finally:
1883        tinfoil.shutdown()
1884
1885    return 0
1886
1887
1888def status(args, config, basepath, workspace):
1889    """Entry point for the devtool 'status' subcommand"""
1890    if workspace:
1891        for recipe, value in sorted(workspace.items()):
1892            recipefile = value['recipefile']
1893            if recipefile:
1894                recipestr = ' (%s)' % recipefile
1895            else:
1896                recipestr = ''
1897            print("%s: %s%s" % (recipe, value['srctree'], recipestr))
1898    else:
1899        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')
1900    return 0
1901
1902
1903def _reset(recipes, no_clean, remove_work, config, basepath, workspace):
1904    """Reset one or more recipes"""
1905    import oe.path
1906
1907    def clean_preferred_provider(pn, layerconf_path):
1908        """Remove PREFERRED_PROVIDER from layer.conf'"""
1909        import re
1910        layerconf_file = os.path.join(layerconf_path, 'conf', 'layer.conf')
1911        new_layerconf_file = os.path.join(layerconf_path, 'conf', '.layer.conf')
1912        pprovider_found = False
1913        with open(layerconf_file, 'r') as f:
1914            lines = f.readlines()
1915            with open(new_layerconf_file, 'a') as nf:
1916                for line in lines:
1917                    pprovider_exp = r'^PREFERRED_PROVIDER_.*? = "' + pn + r'"$'
1918                    if not re.match(pprovider_exp, line):
1919                        nf.write(line)
1920                    else:
1921                        pprovider_found = True
1922        if pprovider_found:
1923            shutil.move(new_layerconf_file, layerconf_file)
1924        else:
1925            os.remove(new_layerconf_file)
1926
1927    if recipes and not no_clean:
1928        if len(recipes) == 1:
1929            logger.info('Cleaning sysroot for recipe %s...' % recipes[0])
1930        else:
1931            logger.info('Cleaning sysroot for recipes %s...' % ', '.join(recipes))
1932        # If the recipe file itself was created in the workspace, and
1933        # it uses BBCLASSEXTEND, then we need to also clean the other
1934        # variants
1935        targets = []
1936        for recipe in recipes:
1937            targets.append(recipe)
1938            recipefile = workspace[recipe]['recipefile']
1939            if recipefile and os.path.exists(recipefile):
1940                targets.extend(get_bbclassextend_targets(recipefile, recipe))
1941        try:
1942            exec_build_env_command(config.init_path, basepath, 'bitbake -c clean %s' % ' '.join(targets))
1943        except bb.process.ExecutionError as e:
1944            raise DevtoolError('Command \'%s\' failed, output:\n%s\nIf you '
1945                                'wish, you may specify -n/--no-clean to '
1946                                'skip running this command when resetting' %
1947                                (e.command, e.stdout))
1948
1949    for pn in recipes:
1950        _check_preserve(config, pn)
1951
1952        appendfile = workspace[pn]['bbappend']
1953        if os.path.exists(appendfile):
1954            # This shouldn't happen, but is possible if devtool errored out prior to
1955            # writing the md5 file. We need to delete this here or the recipe won't
1956            # actually be reset
1957            os.remove(appendfile)
1958
1959        preservepath = os.path.join(config.workspace_path, 'attic', pn, pn)
1960        def preservedir(origdir):
1961            if os.path.exists(origdir):
1962                for root, dirs, files in os.walk(origdir):
1963                    for fn in files:
1964                        logger.warning('Preserving %s in %s' % (fn, preservepath))
1965                        _move_file(os.path.join(origdir, fn),
1966                                   os.path.join(preservepath, fn))
1967                    for dn in dirs:
1968                        preservedir(os.path.join(root, dn))
1969                os.rmdir(origdir)
1970
1971        recipefile = workspace[pn]['recipefile']
1972        if recipefile and oe.path.is_path_parent(config.workspace_path, recipefile):
1973            # This should always be true if recipefile is set, but just in case
1974            preservedir(os.path.dirname(recipefile))
1975        # We don't automatically create this dir next to appends, but the user can
1976        preservedir(os.path.join(config.workspace_path, 'appends', pn))
1977
1978        srctreebase = workspace[pn]['srctreebase']
1979        if os.path.isdir(srctreebase):
1980            if os.listdir(srctreebase):
1981                    if remove_work:
1982                        logger.info('-r argument used on %s, removing source tree.'
1983                                    ' You will lose any unsaved work' %pn)
1984                        shutil.rmtree(srctreebase)
1985                    else:
1986                        # We don't want to risk wiping out any work in progress
1987                        logger.info('Leaving source tree %s as-is; if you no '
1988                                    'longer need it then please delete it manually'
1989                                    % srctreebase)
1990            else:
1991                # This is unlikely, but if it's empty we can just remove it
1992                os.rmdir(srctreebase)
1993
1994        clean_preferred_provider(pn, config.workspace_path)
1995
1996def reset(args, config, basepath, workspace):
1997    """Entry point for the devtool 'reset' subcommand"""
1998    import bb
1999    import shutil
2000
2001    recipes = ""
2002
2003    if args.recipename:
2004        if args.all:
2005            raise DevtoolError("Recipe cannot be specified if -a/--all is used")
2006        else:
2007            for recipe in args.recipename:
2008                check_workspace_recipe(workspace, recipe, checksrc=False)
2009    elif not args.all:
2010        raise DevtoolError("Recipe must be specified, or specify -a/--all to "
2011                           "reset all recipes")
2012    if args.all:
2013        recipes = list(workspace.keys())
2014    else:
2015        recipes = args.recipename
2016
2017    _reset(recipes, args.no_clean, args.remove_work, config, basepath, workspace)
2018
2019    return 0
2020
2021
2022def _get_layer(layername, d):
2023    """Determine the base layer path for the specified layer name/path"""
2024    layerdirs = d.getVar('BBLAYERS').split()
2025    layers = {}    # {basename: layer_paths}
2026    for p in layerdirs:
2027        bn = os.path.basename(p)
2028        if bn not in layers:
2029            layers[bn] = [p]
2030        else:
2031            layers[bn].append(p)
2032    # Provide some shortcuts
2033    if layername.lower() in ['oe-core', 'openembedded-core']:
2034        layername = 'meta'
2035    layer_paths = layers.get(layername, None)
2036    if not layer_paths:
2037        return os.path.abspath(layername)
2038    elif len(layer_paths) == 1:
2039        return os.path.abspath(layer_paths[0])
2040    else:
2041        # multiple layers having the same base name
2042        logger.warning("Multiple layers have the same base name '%s', use the first one '%s'." % (layername, layer_paths[0]))
2043        logger.warning("Consider using path instead of base name to specify layer:\n\t\t%s" % '\n\t\t'.join(layer_paths))
2044        return os.path.abspath(layer_paths[0])
2045
2046
2047def finish(args, config, basepath, workspace):
2048    """Entry point for the devtool 'finish' subcommand"""
2049    import bb
2050    import oe.recipeutils
2051
2052    check_workspace_recipe(workspace, args.recipename)
2053
2054    dry_run_suffix = ' (dry-run)' if args.dry_run else ''
2055
2056    # Grab the equivalent of COREBASE without having to initialise tinfoil
2057    corebasedir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..'))
2058
2059    srctree = workspace[args.recipename]['srctree']
2060    check_git_repo_op(srctree, [corebasedir])
2061    dirty = check_git_repo_dirty(srctree)
2062    if dirty:
2063        if args.force:
2064            logger.warning('Source tree is not clean, continuing as requested by -f/--force')
2065        else:
2066            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)
2067
2068    no_clean = args.no_clean
2069    remove_work=args.remove_work
2070    tinfoil = setup_tinfoil(basepath=basepath, tracking=True)
2071    try:
2072        rd = parse_recipe(config, tinfoil, args.recipename, True)
2073        if not rd:
2074            return 1
2075
2076        destlayerdir = _get_layer(args.destination, tinfoil.config_data)
2077        recipefile = rd.getVar('FILE')
2078        recipedir = os.path.dirname(recipefile)
2079        origlayerdir = oe.recipeutils.find_layerdir(recipefile)
2080
2081        if not os.path.isdir(destlayerdir):
2082            raise DevtoolError('Unable to find layer or directory matching "%s"' % args.destination)
2083
2084        if os.path.abspath(destlayerdir) == config.workspace_path:
2085            raise DevtoolError('"%s" specifies the workspace layer - that is not a valid destination' % args.destination)
2086
2087        # If it's an upgrade, grab the original path
2088        origpath = None
2089        origfilelist = None
2090        append = workspace[args.recipename]['bbappend']
2091        with open(append, 'r') as f:
2092            for line in f:
2093                if line.startswith('# original_path:'):
2094                    origpath = line.split(':')[1].strip()
2095                elif line.startswith('# original_files:'):
2096                    origfilelist = line.split(':')[1].split()
2097
2098        destlayerbasedir = oe.recipeutils.find_layerdir(destlayerdir)
2099
2100        if origlayerdir == config.workspace_path:
2101            # Recipe file itself is in workspace, update it there first
2102            appendlayerdir = None
2103            origrelpath = None
2104            if origpath:
2105                origlayerpath = oe.recipeutils.find_layerdir(origpath)
2106                if origlayerpath:
2107                    origrelpath = os.path.relpath(origpath, origlayerpath)
2108            destpath = oe.recipeutils.get_bbfile_path(rd, destlayerdir, origrelpath)
2109            if not destpath:
2110                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))
2111            # Warn if the layer isn't in bblayers.conf (the code to create a bbappend will do this in other cases)
2112            layerdirs = [os.path.abspath(layerdir) for layerdir in rd.getVar('BBLAYERS').split()]
2113            if not os.path.abspath(destlayerbasedir) in layerdirs:
2114                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)
2115
2116        elif destlayerdir == origlayerdir:
2117            # Same layer, update the original recipe
2118            appendlayerdir = None
2119            destpath = None
2120        else:
2121            # Create/update a bbappend in the specified layer
2122            appendlayerdir = destlayerdir
2123            destpath = None
2124
2125        # Actually update the recipe / bbappend
2126        removing_original = (origpath and origfilelist and oe.recipeutils.find_layerdir(origpath) == destlayerbasedir)
2127        dry_run_output = None
2128        dry_run_outdir = None
2129        if args.dry_run:
2130            dry_run_output = tempfile.TemporaryDirectory(prefix='devtool')
2131            dry_run_outdir = dry_run_output.name
2132        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)
2133        removed = [os.path.relpath(pth, recipedir) for pth in removed]
2134
2135        # Remove any old files in the case of an upgrade
2136        if removing_original:
2137            for fn in origfilelist:
2138                fnp = os.path.join(origpath, fn)
2139                if fn in removed or not os.path.exists(os.path.join(recipedir, fn)):
2140                    logger.info('Removing file %s%s' % (fnp, dry_run_suffix))
2141                if not args.dry_run:
2142                    try:
2143                        os.remove(fnp)
2144                    except FileNotFoundError:
2145                        pass
2146
2147        if origlayerdir == config.workspace_path and destpath:
2148            # Recipe file itself is in the workspace - need to move it and any
2149            # associated files to the specified layer
2150            no_clean = True
2151            logger.info('Moving recipe file to %s%s' % (destpath, dry_run_suffix))
2152            for root, _, files in os.walk(recipedir):
2153                for fn in files:
2154                    srcpath = os.path.join(root, fn)
2155                    relpth = os.path.relpath(os.path.dirname(srcpath), recipedir)
2156                    destdir = os.path.abspath(os.path.join(destpath, relpth))
2157                    destfp = os.path.join(destdir, fn)
2158                    _move_file(srcpath, destfp, dry_run_outdir=dry_run_outdir, base_outdir=destpath)
2159
2160        if dry_run_outdir:
2161            import difflib
2162            comparelist = []
2163            for root, _, files in os.walk(dry_run_outdir):
2164                for fn in files:
2165                    outf = os.path.join(root, fn)
2166                    relf = os.path.relpath(outf, dry_run_outdir)
2167                    logger.debug('dry-run: output file %s' % relf)
2168                    if fn.endswith('.bb'):
2169                        if origfilelist and origpath and destpath:
2170                            # Need to match this up with the pre-upgrade recipe file
2171                            for origf in origfilelist:
2172                                if origf.endswith('.bb'):
2173                                    comparelist.append((os.path.abspath(os.path.join(origpath, origf)),
2174                                                        outf,
2175                                                        os.path.abspath(os.path.join(destpath, relf))))
2176                                    break
2177                        else:
2178                            # Compare to the existing recipe
2179                            comparelist.append((recipefile, outf, recipefile))
2180                    elif fn.endswith('.bbappend'):
2181                        if appendfile:
2182                            if os.path.exists(appendfile):
2183                                comparelist.append((appendfile, outf, appendfile))
2184                            else:
2185                                comparelist.append((None, outf, appendfile))
2186                    else:
2187                        if destpath:
2188                            recipedest = destpath
2189                        elif appendfile:
2190                            recipedest = os.path.dirname(appendfile)
2191                        else:
2192                            recipedest = os.path.dirname(recipefile)
2193                        destfp = os.path.join(recipedest, relf)
2194                        if os.path.exists(destfp):
2195                            comparelist.append((destfp, outf, destfp))
2196            output = ''
2197            for oldfile, newfile, newfileshow in comparelist:
2198                if oldfile:
2199                    with open(oldfile, 'r') as f:
2200                        oldlines = f.readlines()
2201                else:
2202                    oldfile = '/dev/null'
2203                    oldlines = []
2204                with open(newfile, 'r') as f:
2205                    newlines = f.readlines()
2206                if not newfileshow:
2207                    newfileshow = newfile
2208                diff = difflib.unified_diff(oldlines, newlines, oldfile, newfileshow)
2209                difflines = list(diff)
2210                if difflines:
2211                    output += ''.join(difflines)
2212            if output:
2213                logger.info('Diff of changed files:\n%s' % output)
2214    finally:
2215        tinfoil.shutdown()
2216
2217    # Everything else has succeeded, we can now reset
2218    if args.dry_run:
2219        logger.info('Resetting recipe (dry-run)')
2220    else:
2221        _reset([args.recipename], no_clean=no_clean, remove_work=remove_work, config=config, basepath=basepath, workspace=workspace)
2222
2223    return 0
2224
2225
2226def get_default_srctree(config, recipename=''):
2227    """Get the default srctree path"""
2228    srctreeparent = config.get('General', 'default_source_parent_dir', config.workspace_path)
2229    if recipename:
2230        return os.path.join(srctreeparent, 'sources', recipename)
2231    else:
2232        return os.path.join(srctreeparent, 'sources')
2233
2234def register_commands(subparsers, context):
2235    """Register devtool subcommands from this plugin"""
2236
2237    defsrctree = get_default_srctree(context.config)
2238    parser_add = subparsers.add_parser('add', help='Add a new recipe',
2239                                       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.',
2240                                       group='starting', order=100)
2241    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.')
2242    parser_add.add_argument('srctree', nargs='?', help='Path to external source tree. If not specified, a subdirectory of %s will be used.' % defsrctree)
2243    parser_add.add_argument('fetchuri', nargs='?', help='Fetch the specified URI and extract it to create the source tree')
2244    group = parser_add.add_mutually_exclusive_group()
2245    group.add_argument('--same-dir', '-s', help='Build in same directory as source', action="store_true")
2246    group.add_argument('--no-same-dir', help='Force build in a separate build directory', action="store_true")
2247    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')
2248    parser_add.add_argument('--npm-dev', help='For npm, also fetch devDependencies', action="store_true")
2249    parser_add.add_argument('--version', '-V', help='Version to use within recipe (PV)')
2250    parser_add.add_argument('--no-git', '-g', help='If fetching source, do not set up source tree as a git repository', action="store_true")
2251    group = parser_add.add_mutually_exclusive_group()
2252    group.add_argument('--srcrev', '-S', help='Source revision to fetch if fetching from an SCM such as git (default latest)')
2253    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")
2254    parser_add.add_argument('--srcbranch', '-B', help='Branch in source repository if fetching from an SCM such as git (default master)')
2255    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')
2256    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')
2257    parser_add.add_argument('--src-subdir', help='Specify subdirectory within source tree to use', metavar='SUBDIR')
2258    parser_add.add_argument('--mirrors', help='Enable PREMIRRORS and MIRRORS for source tree fetching (disable by default).', action="store_true")
2259    parser_add.add_argument('--provides', '-p', help='Specify an alias for the item provided by the recipe. E.g. virtual/libgl')
2260    parser_add.set_defaults(func=add, fixed_setup=context.fixed_setup)
2261
2262    parser_modify = subparsers.add_parser('modify', help='Modify the source for an existing recipe',
2263                                       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.',
2264                                       group='starting', order=90)
2265    parser_modify.add_argument('recipename', help='Name of existing recipe to edit (just name - no version, path or extension)')
2266    parser_modify.add_argument('srctree', nargs='?', help='Path to external source tree. If not specified, a subdirectory of %s will be used.' % defsrctree)
2267    parser_modify.add_argument('--wildcard', '-w', action="store_true", help='Use wildcard for unversioned bbappend')
2268    group = parser_modify.add_mutually_exclusive_group()
2269    group.add_argument('--extract', '-x', action="store_true", help='Extract source for recipe (default)')
2270    group.add_argument('--no-extract', '-n', action="store_true", help='Do not extract source, expect it to exist')
2271    group = parser_modify.add_mutually_exclusive_group()
2272    group.add_argument('--same-dir', '-s', help='Build in same directory as source', action="store_true")
2273    group.add_argument('--no-same-dir', help='Force build in a separate build directory', action="store_true")
2274    parser_modify.add_argument('--branch', '-b', default="devtool", help='Name for development branch to checkout (when not using -n/--no-extract) (default "%(default)s")')
2275    parser_modify.add_argument('--no-overrides', '-O', action="store_true", help='Do not create branches for other override configurations')
2276    parser_modify.add_argument('--keep-temp', help='Keep temporary directory (for debugging)', action="store_true")
2277    parser_modify.set_defaults(func=modify, fixed_setup=context.fixed_setup)
2278
2279    parser_extract = subparsers.add_parser('extract', help='Extract the source for an existing recipe',
2280                                       description='Extracts the source for an existing recipe',
2281                                       group='advanced')
2282    parser_extract.add_argument('recipename', help='Name of recipe to extract the source for')
2283    parser_extract.add_argument('srctree', help='Path to where to extract the source tree')
2284    parser_extract.add_argument('--branch', '-b', default="devtool", help='Name for development branch to checkout (default "%(default)s")')
2285    parser_extract.add_argument('--no-overrides', '-O', action="store_true", help='Do not create branches for other override configurations')
2286    parser_extract.add_argument('--keep-temp', action="store_true", help='Keep temporary directory (for debugging)')
2287    parser_extract.set_defaults(func=extract, fixed_setup=context.fixed_setup)
2288
2289    parser_sync = subparsers.add_parser('sync', help='Synchronize the source tree for an existing recipe',
2290                                       description='Synchronize the previously extracted source tree for an existing recipe',
2291                                       formatter_class=argparse.ArgumentDefaultsHelpFormatter,
2292                                       group='advanced')
2293    parser_sync.add_argument('recipename', help='Name of recipe to sync the source for')
2294    parser_sync.add_argument('srctree', help='Path to the source tree')
2295    parser_sync.add_argument('--branch', '-b', default="devtool", help='Name for development branch to checkout')
2296    parser_sync.add_argument('--keep-temp', action="store_true", help='Keep temporary directory (for debugging)')
2297    parser_sync.set_defaults(func=sync, fixed_setup=context.fixed_setup)
2298
2299    parser_rename = subparsers.add_parser('rename', help='Rename a recipe file in the workspace',
2300                                       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.',
2301                                       group='working', order=10)
2302    parser_rename.add_argument('recipename', help='Current name of recipe to rename')
2303    parser_rename.add_argument('newname', nargs='?', help='New name for recipe (optional, not needed if you only want to change the version)')
2304    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)')
2305    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')
2306    parser_rename.set_defaults(func=rename)
2307
2308    parser_update_recipe = subparsers.add_parser('update-recipe', help='Apply changes from external source tree to recipe',
2309                                       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.',
2310                                       group='working', order=-90)
2311    parser_update_recipe.add_argument('recipename', help='Name of recipe to update')
2312    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')
2313    parser_update_recipe.add_argument('--initial-rev', help='Override starting revision for patches')
2314    parser_update_recipe.add_argument('--append', '-a', help='Write changes to a bbappend in the specified layer instead of the recipe', metavar='LAYERDIR')
2315    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')
2316    parser_update_recipe.add_argument('--no-remove', '-n', action="store_true", help='Don\'t remove patches, only add or update')
2317    parser_update_recipe.add_argument('--no-overrides', '-O', action="store_true", help='Do not handle other override branches (if they exist)')
2318    parser_update_recipe.add_argument('--dry-run', '-N', action="store_true", help='Dry-run (just report changes instead of writing them)')
2319    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)')
2320    parser_update_recipe.set_defaults(func=update_recipe)
2321
2322    parser_status = subparsers.add_parser('status', help='Show workspace status',
2323                                          description='Lists recipes currently in your workspace and the paths to their respective external source trees',
2324                                          group='info', order=100)
2325    parser_status.set_defaults(func=status)
2326
2327    parser_reset = subparsers.add_parser('reset', help='Remove a recipe from your workspace',
2328                                         description='Removes the specified recipe(s) from your workspace (resetting its state back to that defined by the metadata).',
2329                                         group='working', order=-100)
2330    parser_reset.add_argument('recipename', nargs='*', help='Recipe to reset')
2331    parser_reset.add_argument('--all', '-a', action="store_true", help='Reset all recipes (clear workspace)')
2332    parser_reset.add_argument('--no-clean', '-n', action="store_true", help='Don\'t clean the sysroot to remove recipe output')
2333    parser_reset.add_argument('--remove-work', '-r', action="store_true", help='Clean the sources directory along with append')
2334    parser_reset.set_defaults(func=reset)
2335
2336    parser_finish = subparsers.add_parser('finish', help='Finish working on a recipe in your workspace',
2337                                         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.',
2338                                         group='working', order=-100)
2339    parser_finish.add_argument('recipename', help='Recipe to finish')
2340    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.')
2341    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')
2342    parser_finish.add_argument('--initial-rev', help='Override starting revision for patches')
2343    parser_finish.add_argument('--force', '-f', action="store_true", help='Force continuing even if there are uncommitted changes in the source tree repository')
2344    parser_finish.add_argument('--remove-work', '-r', action="store_true", help='Clean the sources directory under workspace')
2345    parser_finish.add_argument('--no-clean', '-n', action="store_true", help='Don\'t clean the sysroot to remove recipe output')
2346    parser_finish.add_argument('--no-overrides', '-O', action="store_true", help='Do not handle other override branches (if they exist)')
2347    parser_finish.add_argument('--dry-run', '-N', action="store_true", help='Dry-run (just report changes instead of writing them)')
2348    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)')
2349    parser_finish.set_defaults(func=finish)
2350