blob: 4eff6f878b0a912465992cf6a3731e3a089d2431 [file] [log] [blame]
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001# Development tool - standard commands plugin
2#
Patrick Williamsc0f7c042017-02-23 20:41:17 -06003# Copyright (C) 2014-2016 Intel Corporation
Patrick Williamsc124f4f2015-09-15 14:41:29 -05004#
5# This program is free software; you can redistribute it and/or modify
6# it under the terms of the GNU General Public License version 2 as
7# published by the Free Software Foundation.
8#
9# This program is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU General Public License for more details.
13#
14# You should have received a copy of the GNU General Public License along
15# with this program; if not, write to the Free Software Foundation, Inc.,
16# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17"""Devtool standard plugins"""
18
19import os
20import sys
21import re
22import shutil
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050023import subprocess
Patrick Williamsc124f4f2015-09-15 14:41:29 -050024import tempfile
25import logging
26import argparse
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050027import argparse_oe
Patrick Williamsc124f4f2015-09-15 14:41:29 -050028import scriptutils
29import errno
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050030import glob
Patrick Williamsc0f7c042017-02-23 20:41:17 -060031import filecmp
Patrick Williamsf1e5d692016-03-30 15:21:19 -050032from collections import OrderedDict
Patrick Williamsc0f7c042017-02-23 20:41:17 -060033from devtool import exec_build_env_command, setup_tinfoil, check_workspace_recipe, use_external_build, setup_git_repo, recipe_to_append, get_bbclassextend_targets, ensure_npm, DevtoolError
Patrick Williamsc124f4f2015-09-15 14:41:29 -050034from devtool import parse_recipe
35
36logger = logging.getLogger('devtool')
37
38
39def add(args, config, basepath, workspace):
40 """Entry point for the devtool 'add' subcommand"""
41 import bb
42 import oe.recipeutils
43
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050044 if not args.recipename and not args.srctree and not args.fetch and not args.fetchuri:
45 raise argparse_oe.ArgumentUsageError('At least one of recipename, srctree, fetchuri or -f/--fetch must be specified', 'add')
Patrick Williamsc124f4f2015-09-15 14:41:29 -050046
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050047 # These are positional arguments, but because we're nice, allow
48 # specifying e.g. source tree without name, or fetch URI without name or
49 # source tree (if we can detect that that is what the user meant)
Patrick Williamsc0f7c042017-02-23 20:41:17 -060050 if scriptutils.is_src_url(args.recipename):
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050051 if not args.fetchuri:
52 if args.fetch:
53 raise DevtoolError('URI specified as positional argument as well as -f/--fetch')
54 args.fetchuri = args.recipename
55 args.recipename = ''
Patrick Williamsc0f7c042017-02-23 20:41:17 -060056 elif scriptutils.is_src_url(args.srctree):
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050057 if not args.fetchuri:
58 if args.fetch:
59 raise DevtoolError('URI specified as positional argument as well as -f/--fetch')
60 args.fetchuri = args.srctree
61 args.srctree = ''
62 elif args.recipename and not args.srctree:
63 if os.sep in args.recipename:
64 args.srctree = args.recipename
65 args.recipename = None
66 elif os.path.isdir(args.recipename):
Patrick Williamsc0f7c042017-02-23 20:41:17 -060067 logger.warn('Ambiguous argument "%s" - assuming you mean it to be the recipe name' % args.recipename)
68
69 if args.srctree and os.path.isfile(args.srctree):
70 args.fetchuri = 'file://' + os.path.abspath(args.srctree)
71 args.srctree = ''
Patrick Williamsc124f4f2015-09-15 14:41:29 -050072
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050073 if args.fetch:
74 if args.fetchuri:
75 raise DevtoolError('URI specified as positional argument as well as -f/--fetch')
76 else:
Patrick Williamsc0f7c042017-02-23 20:41:17 -060077 logger.warn('-f/--fetch option is deprecated - you can now simply specify the URL to fetch as a positional argument instead')
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050078 args.fetchuri = args.fetch
Patrick Williamsf1e5d692016-03-30 15:21:19 -050079
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050080 if args.recipename:
81 if args.recipename in workspace:
82 raise DevtoolError("recipe %s is already in your workspace" %
83 args.recipename)
84 reason = oe.recipeutils.validate_pn(args.recipename)
85 if reason:
86 raise DevtoolError(reason)
87
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050088 if args.srctree:
89 srctree = os.path.abspath(args.srctree)
90 srctreeparent = None
91 tmpsrcdir = None
92 else:
93 srctree = None
94 srctreeparent = get_default_srctree(config)
95 bb.utils.mkdirhier(srctreeparent)
96 tmpsrcdir = tempfile.mkdtemp(prefix='devtoolsrc', dir=srctreeparent)
97
98 if srctree and os.path.exists(srctree):
99 if args.fetchuri:
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500100 if not os.path.isdir(srctree):
101 raise DevtoolError("Cannot fetch into source tree path %s as "
102 "it exists and is not a directory" %
103 srctree)
104 elif os.listdir(srctree):
105 raise DevtoolError("Cannot fetch into source tree path %s as "
106 "it already exists and is non-empty" %
107 srctree)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500108 elif not args.fetchuri:
109 if args.srctree:
110 raise DevtoolError("Specified source tree %s could not be found" %
111 args.srctree)
112 elif srctree:
113 raise DevtoolError("No source tree exists at default path %s - "
114 "either create and populate this directory, "
115 "or specify a path to a source tree, or a "
116 "URI to fetch source from" % srctree)
117 else:
118 raise DevtoolError("You must either specify a source tree "
119 "or a URI to fetch source from")
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500120
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500121 if args.version:
122 if '_' in args.version or ' ' in args.version:
123 raise DevtoolError('Invalid version string "%s"' % args.version)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500124
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500125 if args.color == 'auto' and sys.stdout.isatty():
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500126 color = 'always'
127 else:
128 color = args.color
129 extracmdopts = ''
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500130 if args.fetchuri:
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600131 if args.fetchuri.startswith('npm://'):
132 ensure_npm(config, basepath, args.fixed_setup)
133
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500134 source = args.fetchuri
135 if srctree:
136 extracmdopts += ' -x %s' % srctree
137 else:
138 extracmdopts += ' -x %s' % tmpsrcdir
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500139 else:
140 source = srctree
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500141 if args.recipename:
142 extracmdopts += ' -N %s' % args.recipename
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500143 if args.version:
144 extracmdopts += ' -V %s' % args.version
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500145 if args.binary:
146 extracmdopts += ' -b'
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500147 if args.also_native:
148 extracmdopts += ' --also-native'
149 if args.src_subdir:
150 extracmdopts += ' --src-subdir "%s"' % args.src_subdir
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600151 if args.autorev:
152 extracmdopts += ' -a'
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500153
154 tempdir = tempfile.mkdtemp(prefix='devtool')
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500155 try:
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600156 while True:
157 try:
158 stdout, _ = exec_build_env_command(config.init_path, basepath, 'recipetool --color=%s create -o %s "%s" %s' % (color, tempdir, source, extracmdopts))
159 except bb.process.ExecutionError as e:
160 if e.exitcode == 14:
161 # FIXME this is a horrible hack that is unfortunately
162 # necessary due to the fact that we can't run bitbake from
163 # inside recipetool since recipetool keeps tinfoil active
164 # with references to it throughout the code, so we have
165 # to exit out and come back here to do it.
166 ensure_npm(config, basepath, args.fixed_setup)
167 continue
168 elif e.exitcode == 15:
169 raise DevtoolError('Could not auto-determine recipe name, please specify it on the command line')
170 else:
171 raise DevtoolError('Command \'%s\' failed:\n%s' % (e.command, e.stdout))
172 break
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500173
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500174 recipes = glob.glob(os.path.join(tempdir, '*.bb'))
175 if recipes:
176 recipename = os.path.splitext(os.path.basename(recipes[0]))[0].split('_')[0]
177 if recipename in workspace:
178 raise DevtoolError('A recipe with the same name as the one being created (%s) already exists in your workspace' % recipename)
179 recipedir = os.path.join(config.workspace_path, 'recipes', recipename)
180 bb.utils.mkdirhier(recipedir)
181 recipefile = os.path.join(recipedir, os.path.basename(recipes[0]))
182 appendfile = recipe_to_append(recipefile, config)
183 if os.path.exists(appendfile):
184 # This shouldn't be possible, but just in case
185 raise DevtoolError('A recipe with the same name as the one being created already exists in your workspace')
186 if os.path.exists(recipefile):
187 raise DevtoolError('A recipe file %s already exists in your workspace; this shouldn\'t be there - please delete it before continuing' % recipefile)
188 if tmpsrcdir:
189 srctree = os.path.join(srctreeparent, recipename)
190 if os.path.exists(tmpsrcdir):
191 if os.path.exists(srctree):
192 if os.path.isdir(srctree):
193 try:
194 os.rmdir(srctree)
195 except OSError as e:
196 if e.errno == errno.ENOTEMPTY:
197 raise DevtoolError('Source tree path %s already exists and is not empty' % srctree)
198 else:
199 raise
200 else:
201 raise DevtoolError('Source tree path %s already exists and is not a directory' % srctree)
202 logger.info('Using default source tree path %s' % srctree)
203 shutil.move(tmpsrcdir, srctree)
204 else:
205 raise DevtoolError('Couldn\'t find source tree created by recipetool')
206 bb.utils.mkdirhier(recipedir)
207 shutil.move(recipes[0], recipefile)
208 # Move any additional files created by recipetool
209 for fn in os.listdir(tempdir):
210 shutil.move(os.path.join(tempdir, fn), recipedir)
211 else:
212 raise DevtoolError('Command \'%s\' did not create any recipe file:\n%s' % (e.command, e.stdout))
213 attic_recipe = os.path.join(config.workspace_path, 'attic', recipename, os.path.basename(recipefile))
214 if os.path.exists(attic_recipe):
215 logger.warn('A modified recipe from a previous invocation exists in %s - you may wish to move this over the top of the new recipe if you had changes in it that you want to continue with' % attic_recipe)
216 finally:
217 if tmpsrcdir and os.path.exists(tmpsrcdir):
218 shutil.rmtree(tmpsrcdir)
219 shutil.rmtree(tempdir)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500220
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500221 for fn in os.listdir(recipedir):
222 _add_md5(config, recipename, os.path.join(recipedir, fn))
223
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500224 tinfoil = setup_tinfoil(config_only=True, basepath=basepath)
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600225 try:
226 rd = oe.recipeutils.parse_recipe(tinfoil.cooker, recipefile, None)
227 if not rd:
228 return 1
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500229
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600230 if args.fetchuri and not args.no_git:
231 setup_git_repo(srctree, args.version, 'devtool', d=tinfoil.config_data)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500232
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600233 initial_rev = None
234 if os.path.exists(os.path.join(srctree, '.git')):
235 (stdout, _) = bb.process.run('git rev-parse HEAD', cwd=srctree)
236 initial_rev = stdout.rstrip()
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500237
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600238 if args.src_subdir:
239 srctree = os.path.join(srctree, args.src_subdir)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500240
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600241 bb.utils.mkdirhier(os.path.dirname(appendfile))
242 with open(appendfile, 'w') as f:
243 f.write('inherit externalsrc\n')
244 f.write('EXTERNALSRC = "%s"\n' % srctree)
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500245
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600246 b_is_s = use_external_build(args.same_dir, args.no_same_dir, rd)
247 if b_is_s:
248 f.write('EXTERNALSRC_BUILD = "%s"\n' % srctree)
249 if initial_rev:
250 f.write('\n# initial_rev: %s\n' % initial_rev)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500251
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600252 if args.binary:
253 f.write('do_install_append() {\n')
254 f.write(' rm -rf ${D}/.git\n')
255 f.write(' rm -f ${D}/singletask.lock\n')
256 f.write('}\n')
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500257
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600258 if bb.data.inherits_class('npm', rd):
259 f.write('do_install_append() {\n')
260 f.write(' # Remove files added to source dir by devtool/externalsrc\n')
261 f.write(' rm -f ${NPM_INSTALLDIR}/singletask.lock\n')
262 f.write(' rm -rf ${NPM_INSTALLDIR}/.git\n')
263 f.write(' rm -rf ${NPM_INSTALLDIR}/oe-local-files\n')
264 f.write(' for symlink in ${EXTERNALSRC_SYMLINKS} ; do\n')
265 f.write(' rm -f ${NPM_INSTALLDIR}/${symlink%%:*}\n')
266 f.write(' done\n')
267 f.write('}\n')
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500268
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600269 _add_md5(config, recipename, appendfile)
270
271 logger.info('Recipe %s has been automatically created; further editing may be required to make it fully functional' % recipefile)
272
273 finally:
274 tinfoil.shutdown()
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500275
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500276 return 0
277
278
279def _check_compatible_recipe(pn, d):
280 """Check if the recipe is supported by devtool"""
281 if pn == 'perf':
282 raise DevtoolError("The perf recipe does not actually check out "
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600283 "source and thus cannot be supported by this tool",
284 4)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500285
286 if pn in ['kernel-devsrc', 'package-index'] or pn.startswith('gcc-source'):
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600287 raise DevtoolError("The %s recipe is not supported by this tool" % pn, 4)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500288
289 if bb.data.inherits_class('image', d):
290 raise DevtoolError("The %s recipe is an image, and therefore is not "
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600291 "supported by this tool" % pn, 4)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500292
293 if bb.data.inherits_class('populate_sdk', d):
294 raise DevtoolError("The %s recipe is an SDK, and therefore is not "
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600295 "supported by this tool" % pn, 4)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500296
297 if bb.data.inherits_class('packagegroup', d):
298 raise DevtoolError("The %s recipe is a packagegroup, and therefore is "
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600299 "not supported by this tool" % pn, 4)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500300
301 if bb.data.inherits_class('meta', d):
302 raise DevtoolError("The %s recipe is a meta-recipe, and therefore is "
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600303 "not supported by this tool" % pn, 4)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500304
305 if bb.data.inherits_class('externalsrc', d) and d.getVar('EXTERNALSRC', True):
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600306 # Not an incompatibility error per se, so we don't pass the error code
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500307 raise DevtoolError("externalsrc is currently enabled for the %s "
308 "recipe. This prevents the normal do_patch task "
309 "from working. You will need to disable this "
310 "first." % pn)
311
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500312def _move_file(src, dst):
313 """Move a file. Creates all the directory components of destination path."""
314 dst_d = os.path.dirname(dst)
315 if dst_d:
316 bb.utils.mkdirhier(dst_d)
317 shutil.move(src, dst)
318
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600319def _copy_file(src, dst):
320 """Copy a file. Creates all the directory components of destination path."""
321 dst_d = os.path.dirname(dst)
322 if dst_d:
323 bb.utils.mkdirhier(dst_d)
324 shutil.copy(src, dst)
325
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500326def _git_ls_tree(repodir, treeish='HEAD', recursive=False):
327 """List contents of a git treeish"""
328 import bb
329 cmd = ['git', 'ls-tree', '-z', treeish]
330 if recursive:
331 cmd.append('-r')
332 out, _ = bb.process.run(cmd, cwd=repodir)
333 ret = {}
334 for line in out.split('\0'):
335 if line:
336 split = line.split(None, 4)
337 ret[split[3]] = split[0:3]
338 return ret
339
340def _git_exclude_path(srctree, path):
341 """Return pathspec (list of paths) that excludes certain path"""
342 # NOTE: "Filtering out" files/paths in this way is not entirely reliable -
343 # we don't catch files that are deleted, for example. A more reliable way
344 # to implement this would be to use "negative pathspecs" which were
345 # introduced in Git v1.9.0. Revisit this when/if the required Git version
346 # becomes greater than that.
347 path = os.path.normpath(path)
348 recurse = True if len(path.split(os.path.sep)) > 1 else False
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600349 git_files = list(_git_ls_tree(srctree, 'HEAD', recurse).keys())
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500350 if path in git_files:
351 git_files.remove(path)
352 return git_files
353 else:
354 return ['.']
355
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500356def _ls_tree(directory):
357 """Recursive listing of files in a directory"""
358 ret = []
359 for root, dirs, files in os.walk(directory):
360 ret.extend([os.path.relpath(os.path.join(root, fname), directory) for
361 fname in files])
362 return ret
363
364
365def extract(args, config, basepath, workspace):
366 """Entry point for the devtool 'extract' subcommand"""
367 import bb
368
369 tinfoil = _prep_extract_operation(config, basepath, args.recipename)
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500370 if not tinfoil:
371 # Error already shown
372 return 1
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600373 try:
374 rd = parse_recipe(config, tinfoil, args.recipename, True)
375 if not rd:
376 return 1
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500377
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600378 srctree = os.path.abspath(args.srctree)
379 initial_rev = _extract_source(srctree, args.keep_temp, args.branch, False, rd)
380 logger.info('Source tree extracted to %s' % srctree)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500381
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600382 if initial_rev:
383 return 0
384 else:
385 return 1
386 finally:
387 tinfoil.shutdown()
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500388
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500389def sync(args, config, basepath, workspace):
390 """Entry point for the devtool 'sync' subcommand"""
391 import bb
392
393 tinfoil = _prep_extract_operation(config, basepath, args.recipename)
394 if not tinfoil:
395 # Error already shown
396 return 1
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600397 try:
398 rd = parse_recipe(config, tinfoil, args.recipename, True)
399 if not rd:
400 return 1
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500401
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600402 srctree = os.path.abspath(args.srctree)
403 initial_rev = _extract_source(srctree, args.keep_temp, args.branch, True, rd)
404 logger.info('Source tree %s synchronized' % srctree)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500405
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600406 if initial_rev:
407 return 0
408 else:
409 return 1
410 finally:
411 tinfoil.shutdown()
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500412
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500413class BbTaskExecutor(object):
414 """Class for executing bitbake tasks for a recipe
415
416 FIXME: This is very awkward. Unfortunately it's not currently easy to
417 properly execute tasks outside of bitbake itself, until then this has to
418 suffice if we are to handle e.g. linux-yocto's extra tasks
419 """
420
421 def __init__(self, rdata):
422 self.rdata = rdata
423 self.executed = []
424
425 def exec_func(self, func, report):
426 """Run bitbake task function"""
427 if not func in self.executed:
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500428 deps = self.rdata.getVarFlag(func, 'deps', False)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500429 if deps:
430 for taskdepfunc in deps:
431 self.exec_func(taskdepfunc, True)
432 if report:
433 logger.info('Executing %s...' % func)
434 fn = self.rdata.getVar('FILE', True)
435 localdata = bb.build._task_data(fn, func, self.rdata)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500436 try:
437 bb.build.exec_func(func, localdata)
438 except bb.build.FuncFailed as e:
439 raise DevtoolError(str(e))
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500440 self.executed.append(func)
441
442
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500443class PatchTaskExecutor(BbTaskExecutor):
444 def __init__(self, rdata):
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600445 import oe.patch
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500446 self.check_git = False
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600447 self.useroptions = []
448 oe.patch.GitApplyTree.gitCommandUserOptions(self.useroptions, d=rdata)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500449 super(PatchTaskExecutor, self).__init__(rdata)
450
451 def exec_func(self, func, report):
452 from oe.patch import GitApplyTree
453 srcsubdir = self.rdata.getVar('S', True)
454 haspatches = False
455 if func == 'do_patch':
456 patchdir = os.path.join(srcsubdir, 'patches')
457 if os.path.exists(patchdir):
458 if os.listdir(patchdir):
459 haspatches = True
460 else:
461 os.rmdir(patchdir)
462
463 super(PatchTaskExecutor, self).exec_func(func, report)
464 if self.check_git and os.path.exists(srcsubdir):
465 if func == 'do_patch':
466 if os.path.exists(patchdir):
467 shutil.rmtree(patchdir)
468 if haspatches:
469 stdout, _ = bb.process.run('git status --porcelain patches', cwd=srcsubdir)
470 if stdout:
471 bb.process.run('git checkout patches', cwd=srcsubdir)
472
473 stdout, _ = bb.process.run('git status --porcelain', cwd=srcsubdir)
474 if stdout:
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600475 bb.process.run('git add .; git %s commit -a -m "Committing changes from %s\n\n%s"' % (' '.join(self.useroptions), func, GitApplyTree.ignore_commit_prefix + ' - from %s' % func), cwd=srcsubdir)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500476
477
478def _prep_extract_operation(config, basepath, recipename, tinfoil=None):
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500479 """HACK: Ugly workaround for making sure that requirements are met when
480 trying to extract a package. Returns the tinfoil instance to be used."""
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500481 if not tinfoil:
482 tinfoil = setup_tinfoil(basepath=basepath)
483
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500484 rd = parse_recipe(config, tinfoil, recipename, True)
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500485 if not rd:
486 return None
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500487
488 if bb.data.inherits_class('kernel-yocto', rd):
489 tinfoil.shutdown()
490 try:
491 stdout, _ = exec_build_env_command(config.init_path, basepath,
492 'bitbake kern-tools-native')
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500493 tinfoil = setup_tinfoil(basepath=basepath)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500494 except bb.process.ExecutionError as err:
495 raise DevtoolError("Failed to build kern-tools-native:\n%s" %
496 err.stdout)
497 return tinfoil
498
499
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500500def _extract_source(srctree, keep_temp, devbranch, sync, d):
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500501 """Extract sources of a recipe"""
502 import bb.event
503 import oe.recipeutils
504
505 def eventfilter(name, handler, event, d):
506 """Bitbake event filter for devtool extract operation"""
507 if name == 'base_eventhandler':
508 return True
509 else:
510 return False
511
512 if hasattr(bb.event, 'set_eventfilter'):
513 bb.event.set_eventfilter(eventfilter)
514
515 pn = d.getVar('PN', True)
516
517 _check_compatible_recipe(pn, d)
518
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500519 if sync:
520 if not os.path.exists(srctree):
521 raise DevtoolError("output path %s does not exist" % srctree)
522 else:
523 if os.path.exists(srctree):
524 if not os.path.isdir(srctree):
525 raise DevtoolError("output path %s exists and is not a directory" %
526 srctree)
527 elif os.listdir(srctree):
528 raise DevtoolError("output path %s already exists and is "
529 "non-empty" % srctree)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500530
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500531 if 'noexec' in (d.getVarFlags('do_unpack', False) or []):
532 raise DevtoolError("The %s recipe has do_unpack disabled, unable to "
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600533 "extract source" % pn, 4)
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500534
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500535 if not sync:
536 # Prepare for shutil.move later on
537 bb.utils.mkdirhier(srctree)
538 os.rmdir(srctree)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500539
540 # We don't want notes to be printed, they are too verbose
541 origlevel = bb.logger.getEffectiveLevel()
542 if logger.getEffectiveLevel() > logging.DEBUG:
543 bb.logger.setLevel(logging.WARNING)
544
545 initial_rev = None
546 tempdir = tempfile.mkdtemp(prefix='devtool')
547 try:
548 crd = d.createCopy()
549 # Make a subdir so we guard against WORKDIR==S
550 workdir = os.path.join(tempdir, 'workdir')
551 crd.setVar('WORKDIR', workdir)
552 crd.setVar('T', os.path.join(tempdir, 'temp'))
553 if not crd.getVar('S', True).startswith(workdir):
554 # Usually a shared workdir recipe (kernel, gcc)
555 # Try to set a reasonable default
556 if bb.data.inherits_class('kernel', d):
557 crd.setVar('S', '${WORKDIR}/source')
558 else:
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500559 crd.setVar('S', '${WORKDIR}/%s' % os.path.basename(d.getVar('S', True)))
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500560 if bb.data.inherits_class('kernel', d):
561 # We don't want to move the source to STAGING_KERNEL_DIR here
562 crd.setVar('STAGING_KERNEL_DIR', '${S}')
563
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500564 task_executor = PatchTaskExecutor(crd)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500565
566 crd.setVar('EXTERNALSRC_forcevariable', '')
567
568 logger.info('Fetching %s...' % pn)
569 task_executor.exec_func('do_fetch', False)
570 logger.info('Unpacking...')
571 task_executor.exec_func('do_unpack', False)
572 if bb.data.inherits_class('kernel-yocto', d):
573 # Extra step for kernel to populate the source directory
574 logger.info('Doing kernel checkout...')
575 task_executor.exec_func('do_kernel_checkout', False)
576 srcsubdir = crd.getVar('S', True)
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500577
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500578 task_executor.check_git = True
579
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500580 # Move local source files into separate subdir
581 recipe_patches = [os.path.basename(patch) for patch in
582 oe.recipeutils.get_recipe_patches(crd)]
583 local_files = oe.recipeutils.get_recipe_local_files(crd)
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600584
585 # Ignore local files with subdir={BP}
586 srcabspath = os.path.abspath(srcsubdir)
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500587 local_files = [fname for fname in local_files if
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600588 os.path.exists(os.path.join(workdir, fname)) and
589 (srcabspath == workdir or not
590 os.path.join(workdir, fname).startswith(srcabspath +
591 os.sep))]
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500592 if local_files:
593 for fname in local_files:
594 _move_file(os.path.join(workdir, fname),
595 os.path.join(tempdir, 'oe-local-files', fname))
596 with open(os.path.join(tempdir, 'oe-local-files', '.gitignore'),
597 'w') as f:
598 f.write('# Ignore local files, by default. Remove this file '
599 'if you want to commit the directory to Git\n*\n')
600
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500601 if srcsubdir == workdir:
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500602 # Find non-patch non-local sources that were "unpacked" to srctree
603 # directory
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500604 src_files = [fname for fname in _ls_tree(workdir) if
605 os.path.basename(fname) not in recipe_patches]
606 # Force separate S so that patch files can be left out from srctree
607 srcsubdir = tempfile.mkdtemp(dir=workdir)
608 crd.setVar('S', srcsubdir)
609 # Move source files to S
610 for path in src_files:
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500611 _move_file(os.path.join(workdir, path),
612 os.path.join(srcsubdir, path))
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500613 elif os.path.dirname(srcsubdir) != workdir:
614 # Handle if S is set to a subdirectory of the source
615 srcsubdir = os.path.join(workdir, os.path.relpath(srcsubdir, workdir).split(os.sep)[0])
616
617 scriptutils.git_convert_standalone_clone(srcsubdir)
618
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500619 # Make sure that srcsubdir exists
620 bb.utils.mkdirhier(srcsubdir)
621 if not os.path.exists(srcsubdir) or not os.listdir(srcsubdir):
622 logger.warning("no source unpacked to S, either the %s recipe "
623 "doesn't use any source or the correct source "
624 "directory could not be determined" % pn)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500625
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600626 setup_git_repo(srcsubdir, crd.getVar('PV', True), devbranch, d=d)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500627
628 (stdout, _) = bb.process.run('git rev-parse HEAD', cwd=srcsubdir)
629 initial_rev = stdout.rstrip()
630
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500631 crd.setVar('PATCHTOOL', 'git')
632
633 logger.info('Patching...')
634 task_executor.exec_func('do_patch', False)
635
636 bb.process.run('git tag -f devtool-patched', cwd=srcsubdir)
637
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500638 kconfig = None
639 if bb.data.inherits_class('kernel-yocto', d):
640 # Store generate and store kernel config
641 logger.info('Generating kernel config')
642 task_executor.exec_func('do_configure', False)
643 kconfig = os.path.join(crd.getVar('B', True), '.config')
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500644
645
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500646 tempdir_localdir = os.path.join(tempdir, 'oe-local-files')
647 srctree_localdir = os.path.join(srctree, 'oe-local-files')
648
649 if sync:
650 bb.process.run('git fetch file://' + srcsubdir + ' ' + devbranch + ':' + devbranch, cwd=srctree)
651
652 # Move oe-local-files directory to srctree
653 # As the oe-local-files is not part of the constructed git tree,
654 # remove them directly during the synchrounizating might surprise
655 # the users. Instead, we move it to oe-local-files.bak and remind
656 # user in the log message.
657 if os.path.exists(srctree_localdir + '.bak'):
658 shutil.rmtree(srctree_localdir, srctree_localdir + '.bak')
659
660 if os.path.exists(srctree_localdir):
661 logger.info('Backing up current local file directory %s' % srctree_localdir)
662 shutil.move(srctree_localdir, srctree_localdir + '.bak')
663
664 if os.path.exists(tempdir_localdir):
665 logger.info('Syncing local source files to srctree...')
666 shutil.copytree(tempdir_localdir, srctree_localdir)
667 else:
668 # Move oe-local-files directory to srctree
669 if os.path.exists(tempdir_localdir):
670 logger.info('Adding local source files to srctree...')
671 shutil.move(tempdir_localdir, srcsubdir)
672
673 shutil.move(srcsubdir, srctree)
674
675 if kconfig:
676 logger.info('Copying kernel config to srctree')
677 shutil.copy2(kconfig, srctree)
678
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500679 finally:
680 bb.logger.setLevel(origlevel)
681
682 if keep_temp:
683 logger.info('Preserving temporary directory %s' % tempdir)
684 else:
685 shutil.rmtree(tempdir)
686 return initial_rev
687
688def _add_md5(config, recipename, filename):
689 """Record checksum of a file (or recursively for a directory) to the md5-file of the workspace"""
690 import bb.utils
691
692 def addfile(fn):
693 md5 = bb.utils.md5_file(fn)
694 with open(os.path.join(config.workspace_path, '.devtool_md5'), 'a') as f:
695 f.write('%s|%s|%s\n' % (recipename, os.path.relpath(fn, config.workspace_path), md5))
696
697 if os.path.isdir(filename):
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500698 for root, _, files in os.walk(filename):
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500699 for f in files:
700 addfile(os.path.join(root, f))
701 else:
702 addfile(filename)
703
704def _check_preserve(config, recipename):
705 """Check if a file was manually changed and needs to be saved in 'attic'
706 directory"""
707 import bb.utils
708 origfile = os.path.join(config.workspace_path, '.devtool_md5')
709 newfile = os.path.join(config.workspace_path, '.devtool_md5_new')
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500710 preservepath = os.path.join(config.workspace_path, 'attic', recipename)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500711 with open(origfile, 'r') as f:
712 with open(newfile, 'w') as tf:
713 for line in f.readlines():
714 splitline = line.rstrip().split('|')
715 if splitline[0] == recipename:
716 removefile = os.path.join(config.workspace_path, splitline[1])
717 try:
718 md5 = bb.utils.md5_file(removefile)
719 except IOError as err:
720 if err.errno == 2:
721 # File no longer exists, skip it
722 continue
723 else:
724 raise
725 if splitline[2] != md5:
726 bb.utils.mkdirhier(preservepath)
727 preservefile = os.path.basename(removefile)
728 logger.warn('File %s modified since it was written, preserving in %s' % (preservefile, preservepath))
729 shutil.move(removefile, os.path.join(preservepath, preservefile))
730 else:
731 os.remove(removefile)
732 else:
733 tf.write(line)
734 os.rename(newfile, origfile)
735
736def modify(args, config, basepath, workspace):
737 """Entry point for the devtool 'modify' subcommand"""
738 import bb
739 import oe.recipeutils
740
741 if args.recipename in workspace:
742 raise DevtoolError("recipe %s is already in your workspace" %
743 args.recipename)
744
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500745 tinfoil = setup_tinfoil(basepath=basepath)
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600746 try:
747 rd = parse_recipe(config, tinfoil, args.recipename, True)
748 if not rd:
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500749 return 1
750
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600751 pn = rd.getVar('PN', True)
752 if pn != args.recipename:
753 logger.info('Mapping %s to %s' % (args.recipename, pn))
754 if pn in workspace:
755 raise DevtoolError("recipe %s is already in your workspace" %
756 pn)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500757
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600758 if args.srctree:
759 srctree = os.path.abspath(args.srctree)
760 else:
761 srctree = get_default_srctree(config, pn)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500762
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600763 if args.no_extract and not os.path.isdir(srctree):
764 raise DevtoolError("--no-extract specified and source path %s does "
765 "not exist or is not a directory" %
766 srctree)
767 if not args.no_extract:
768 tinfoil = _prep_extract_operation(config, basepath, pn, tinfoil)
769 if not tinfoil:
770 # Error already shown
771 return 1
772
773 recipefile = rd.getVar('FILE', True)
774 appendfile = recipe_to_append(recipefile, config, args.wildcard)
775 if os.path.exists(appendfile):
776 raise DevtoolError("Another variant of recipe %s is already in your "
777 "workspace (only one variant of a recipe can "
778 "currently be worked on at once)"
779 % pn)
780
781 _check_compatible_recipe(pn, rd)
782
783 initial_rev = None
784 commits = []
785 if not args.no_extract:
786 initial_rev = _extract_source(srctree, False, args.branch, False, rd)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500787 if not initial_rev:
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600788 return 1
789 logger.info('Source tree extracted to %s' % srctree)
790 # Get list of commits since this revision
791 (stdout, _) = bb.process.run('git rev-list --reverse %s..HEAD' % initial_rev, cwd=srctree)
792 commits = stdout.split()
793 else:
794 if os.path.exists(os.path.join(srctree, '.git')):
795 # Check if it's a tree previously extracted by us
796 try:
797 (stdout, _) = bb.process.run('git branch --contains devtool-base', cwd=srctree)
798 except bb.process.ExecutionError:
799 stdout = ''
800 for line in stdout.splitlines():
801 if line.startswith('*'):
802 (stdout, _) = bb.process.run('git rev-parse devtool-base', cwd=srctree)
803 initial_rev = stdout.rstrip()
804 if not initial_rev:
805 # Otherwise, just grab the head revision
806 (stdout, _) = bb.process.run('git rev-parse HEAD', cwd=srctree)
807 initial_rev = stdout.rstrip()
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500808
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600809 # Check that recipe isn't using a shared workdir
810 s = os.path.abspath(rd.getVar('S', True))
811 workdir = os.path.abspath(rd.getVar('WORKDIR', True))
812 if s.startswith(workdir) and s != workdir and os.path.dirname(s) != workdir:
813 # Handle if S is set to a subdirectory of the source
814 srcsubdir = os.path.relpath(s, workdir).split(os.sep, 1)[1]
815 srctree = os.path.join(srctree, srcsubdir)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500816
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600817 bb.utils.mkdirhier(os.path.dirname(appendfile))
818 with open(appendfile, 'w') as f:
819 f.write('FILESEXTRAPATHS_prepend := "${THISDIR}/${PN}:"\n')
820 # Local files can be modified/tracked in separate subdir under srctree
821 # Mostly useful for packages with S != WORKDIR
822 f.write('FILESPATH_prepend := "%s:"\n' %
823 os.path.join(srctree, 'oe-local-files'))
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500824
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600825 f.write('\ninherit externalsrc\n')
826 f.write('# NOTE: We use pn- overrides here to avoid affecting multiple variants in the case where the recipe uses BBCLASSEXTEND\n')
827 f.write('EXTERNALSRC_pn-%s = "%s"\n' % (pn, srctree))
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500828
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600829 b_is_s = use_external_build(args.same_dir, args.no_same_dir, rd)
830 if b_is_s:
831 f.write('EXTERNALSRC_BUILD_pn-%s = "%s"\n' % (pn, srctree))
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500832
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600833 if bb.data.inherits_class('kernel', rd):
834 f.write('SRCTREECOVEREDTASKS = "do_validate_branches do_kernel_checkout '
835 'do_fetch do_unpack do_patch do_kernel_configme do_kernel_configcheck"\n')
836 f.write('\ndo_configure_append() {\n'
837 ' cp ${B}/.config ${S}/.config.baseline\n'
838 ' ln -sfT ${B}/.config ${S}/.config.new\n'
839 '}\n')
840 if initial_rev:
841 f.write('\n# initial_rev: %s\n' % initial_rev)
842 for commit in commits:
843 f.write('# commit: %s\n' % commit)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500844
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600845 _add_md5(config, pn, appendfile)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500846
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600847 logger.info('Recipe %s now set up to build from %s' % (pn, srctree))
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500848
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600849 finally:
850 tinfoil.shutdown()
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500851
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500852 return 0
853
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600854def _get_patchset_revs(srctree, recipe_path, initial_rev=None):
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500855 """Get initial and update rev of a recipe. These are the start point of the
856 whole patchset and start point for the patches to be re-generated/updated.
857 """
858 import bb
859
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600860 # Parse initial rev from recipe if not specified
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500861 commits = []
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500862 with open(recipe_path, 'r') as f:
863 for line in f:
864 if line.startswith('# initial_rev:'):
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600865 if not initial_rev:
866 initial_rev = line.split(':')[-1].strip()
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500867 elif line.startswith('# commit:'):
868 commits.append(line.split(':')[-1].strip())
869
870 update_rev = initial_rev
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500871 changed_revs = None
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500872 if initial_rev:
873 # Find first actually changed revision
874 stdout, _ = bb.process.run('git rev-list --reverse %s..HEAD' %
875 initial_rev, cwd=srctree)
876 newcommits = stdout.split()
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600877 for i in range(min(len(commits), len(newcommits))):
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500878 if newcommits[i] == commits[i]:
879 update_rev = commits[i]
880
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500881 try:
882 stdout, _ = bb.process.run('git cherry devtool-patched',
883 cwd=srctree)
884 except bb.process.ExecutionError as err:
885 stdout = None
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500886
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500887 if stdout is not None:
888 changed_revs = []
889 for line in stdout.splitlines():
890 if line.startswith('+ '):
891 rev = line.split()[1]
892 if rev in newcommits:
893 changed_revs.append(rev)
894
895 return initial_rev, update_rev, changed_revs
896
897def _remove_file_entries(srcuri, filelist):
898 """Remove file:// entries from SRC_URI"""
899 remaining = filelist[:]
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500900 entries = []
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500901 for fname in filelist:
902 basename = os.path.basename(fname)
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600903 for i in range(len(srcuri)):
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500904 if (srcuri[i].startswith('file://') and
905 os.path.basename(srcuri[i].split(';')[0]) == basename):
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500906 entries.append(srcuri[i])
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500907 remaining.remove(fname)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500908 srcuri.pop(i)
909 break
910 return entries, remaining
911
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600912def _remove_source_files(append, files, destpath):
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500913 """Unlink existing patch files"""
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500914 for path in files:
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600915 if append:
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500916 if not destpath:
917 raise Exception('destpath should be set here')
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500918 path = os.path.join(destpath, os.path.basename(path))
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500919
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500920 if os.path.exists(path):
921 logger.info('Removing file %s' % path)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500922 # FIXME "git rm" here would be nice if the file in question is
923 # tracked
924 # FIXME there's a chance that this file is referred to by
925 # another recipe, in which case deleting wouldn't be the
926 # right thing to do
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500927 os.remove(path)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500928 # Remove directory if empty
929 try:
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500930 os.rmdir(os.path.dirname(path))
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500931 except OSError as ose:
932 if ose.errno != errno.ENOTEMPTY:
933 raise
934
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500935
936def _export_patches(srctree, rd, start_rev, destdir):
937 """Export patches from srctree to given location.
938 Returns three-tuple of dicts:
939 1. updated - patches that already exist in SRCURI
940 2. added - new patches that don't exist in SRCURI
941 3 removed - patches that exist in SRCURI but not in exported patches
942 In each dict the key is the 'basepath' of the URI and value is the
943 absolute path to the existing file in recipe space (if any).
944 """
945 import oe.recipeutils
946 from oe.patch import GitApplyTree
947 updated = OrderedDict()
948 added = OrderedDict()
949 seqpatch_re = re.compile('^([0-9]{4}-)?(.+)')
950
951 existing_patches = dict((os.path.basename(path), path) for path in
952 oe.recipeutils.get_recipe_patches(rd))
953
954 # Generate patches from Git, exclude local files directory
955 patch_pathspec = _git_exclude_path(srctree, 'oe-local-files')
956 GitApplyTree.extractPatches(srctree, start_rev, destdir, patch_pathspec)
957
958 new_patches = sorted(os.listdir(destdir))
959 for new_patch in new_patches:
960 # Strip numbering from patch names. If it's a git sequence named patch,
961 # the numbers might not match up since we are starting from a different
962 # revision This does assume that people are using unique shortlog
963 # values, but they ought to be anyway...
964 new_basename = seqpatch_re.match(new_patch).group(2)
965 found = False
966 for old_patch in existing_patches:
967 old_basename = seqpatch_re.match(old_patch).group(2)
968 if new_basename == old_basename:
969 updated[new_patch] = existing_patches.pop(old_patch)
970 found = True
971 # Rename patch files
972 if new_patch != old_patch:
973 os.rename(os.path.join(destdir, new_patch),
974 os.path.join(destdir, old_patch))
975 break
976 if not found:
977 added[new_patch] = None
978 return (updated, added, existing_patches)
979
980
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500981def _create_kconfig_diff(srctree, rd, outfile):
982 """Create a kconfig fragment"""
983 # Only update config fragment if both config files exist
984 orig_config = os.path.join(srctree, '.config.baseline')
985 new_config = os.path.join(srctree, '.config.new')
986 if os.path.exists(orig_config) and os.path.exists(new_config):
987 cmd = ['diff', '--new-line-format=%L', '--old-line-format=',
988 '--unchanged-line-format=', orig_config, new_config]
989 pipe = subprocess.Popen(cmd, stdout=subprocess.PIPE,
990 stderr=subprocess.PIPE)
991 stdout, stderr = pipe.communicate()
992 if pipe.returncode == 1:
993 logger.info("Updating config fragment %s" % outfile)
994 with open(outfile, 'w') as fobj:
995 fobj.write(stdout)
996 elif pipe.returncode == 0:
997 logger.info("Would remove config fragment %s" % outfile)
998 if os.path.exists(outfile):
999 # Remove fragment file in case of empty diff
1000 logger.info("Removing config fragment %s" % outfile)
1001 os.unlink(outfile)
1002 else:
1003 raise bb.process.ExecutionError(cmd, pipe.returncode, stdout, stderr)
1004 return True
1005 return False
1006
1007
Patrick Williamsf1e5d692016-03-30 15:21:19 -05001008def _export_local_files(srctree, rd, destdir):
1009 """Copy local files from srctree to given location.
1010 Returns three-tuple of dicts:
1011 1. updated - files that already exist in SRCURI
1012 2. added - new files files that don't exist in SRCURI
1013 3 removed - files that exist in SRCURI but not in exported files
1014 In each dict the key is the 'basepath' of the URI and value is the
1015 absolute path to the existing file in recipe space (if any).
1016 """
1017 import oe.recipeutils
1018
1019 # Find out local files (SRC_URI files that exist in the "recipe space").
1020 # Local files that reside in srctree are not included in patch generation.
1021 # Instead they are directly copied over the original source files (in
1022 # recipe space).
1023 existing_files = oe.recipeutils.get_recipe_local_files(rd)
1024 new_set = None
1025 updated = OrderedDict()
1026 added = OrderedDict()
1027 removed = OrderedDict()
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001028 local_files_dir = os.path.join(srctree, 'oe-local-files')
Patrick Williamsf1e5d692016-03-30 15:21:19 -05001029 git_files = _git_ls_tree(srctree)
1030 if 'oe-local-files' in git_files:
1031 # If tracked by Git, take the files from srctree HEAD. First get
1032 # the tree object of the directory
1033 tmp_index = os.path.join(srctree, '.git', 'index.tmp.devtool')
1034 tree = git_files['oe-local-files'][2]
1035 bb.process.run(['git', 'checkout', tree, '--', '.'], cwd=srctree,
1036 env=dict(os.environ, GIT_WORK_TREE=destdir,
1037 GIT_INDEX_FILE=tmp_index))
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001038 new_set = list(_git_ls_tree(srctree, tree, True).keys())
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001039 elif os.path.isdir(local_files_dir):
Patrick Williamsf1e5d692016-03-30 15:21:19 -05001040 # If not tracked by Git, just copy from working copy
1041 new_set = _ls_tree(os.path.join(srctree, 'oe-local-files'))
1042 bb.process.run(['cp', '-ax',
1043 os.path.join(srctree, 'oe-local-files', '.'), destdir])
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001044 else:
1045 new_set = []
1046
1047 # Special handling for kernel config
1048 if bb.data.inherits_class('kernel-yocto', rd):
1049 fragment_fn = 'devtool-fragment.cfg'
1050 fragment_path = os.path.join(destdir, fragment_fn)
1051 if _create_kconfig_diff(srctree, rd, fragment_path):
1052 if os.path.exists(fragment_path):
1053 if fragment_fn not in new_set:
1054 new_set.append(fragment_fn)
1055 # Copy fragment to local-files
1056 if os.path.isdir(local_files_dir):
1057 shutil.copy2(fragment_path, local_files_dir)
1058 else:
1059 if fragment_fn in new_set:
1060 new_set.remove(fragment_fn)
1061 # Remove fragment from local-files
1062 if os.path.exists(os.path.join(local_files_dir, fragment_fn)):
1063 os.unlink(os.path.join(local_files_dir, fragment_fn))
1064
Patrick Williamsf1e5d692016-03-30 15:21:19 -05001065 if new_set is not None:
1066 for fname in new_set:
1067 if fname in existing_files:
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001068 origpath = existing_files.pop(fname)
1069 workpath = os.path.join(local_files_dir, fname)
1070 if not filecmp.cmp(origpath, workpath):
1071 updated[fname] = origpath
Patrick Williamsf1e5d692016-03-30 15:21:19 -05001072 elif fname != '.gitignore':
1073 added[fname] = None
1074
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001075 workdir = rd.getVar('WORKDIR', True)
1076 s = rd.getVar('S', True)
1077 if not s.endswith(os.sep):
1078 s += os.sep
1079
1080 if workdir != s:
1081 # Handle files where subdir= was specified
1082 for fname in list(existing_files.keys()):
1083 # FIXME handle both subdir starting with BP and not?
1084 fworkpath = os.path.join(workdir, fname)
1085 if fworkpath.startswith(s):
1086 fpath = os.path.join(srctree, os.path.relpath(fworkpath, s))
1087 if os.path.exists(fpath):
1088 origpath = existing_files.pop(fname)
1089 if not filecmp.cmp(origpath, fpath):
1090 updated[fpath] = origpath
1091
Patrick Williamsf1e5d692016-03-30 15:21:19 -05001092 removed = existing_files
1093 return (updated, added, removed)
1094
1095
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001096def _determine_files_dir(rd):
1097 """Determine the appropriate files directory for a recipe"""
1098 recipedir = rd.getVar('FILE_DIRNAME', True)
1099 for entry in rd.getVar('FILESPATH', True).split(':'):
1100 relpth = os.path.relpath(entry, recipedir)
1101 if not os.sep in relpth:
1102 # One (or zero) levels below only, so we don't put anything in machine-specific directories
1103 if os.path.isdir(entry):
1104 return entry
1105 return os.path.join(recipedir, rd.getVar('BPN', True))
1106
1107
1108def _update_recipe_srcrev(srctree, rd, appendlayerdir, wildcard_version, no_remove):
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001109 """Implement the 'srcrev' mode of update-recipe"""
1110 import bb
1111 import oe.recipeutils
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001112
1113 recipefile = rd.getVar('FILE', True)
1114 logger.info('Updating SRCREV in recipe %s' % os.path.basename(recipefile))
1115
1116 # Get HEAD revision
1117 try:
1118 stdout, _ = bb.process.run('git rev-parse HEAD', cwd=srctree)
1119 except bb.process.ExecutionError as err:
1120 raise DevtoolError('Failed to get HEAD revision in %s: %s' %
1121 (srctree, err))
1122 srcrev = stdout.strip()
1123 if len(srcrev) != 40:
1124 raise DevtoolError('Invalid hash returned by git: %s' % stdout)
1125
1126 destpath = None
Patrick Williamsf1e5d692016-03-30 15:21:19 -05001127 remove_files = []
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001128 patchfields = {}
1129 patchfields['SRCREV'] = srcrev
1130 orig_src_uri = rd.getVar('SRC_URI', False) or ''
Patrick Williamsf1e5d692016-03-30 15:21:19 -05001131 srcuri = orig_src_uri.split()
1132 tempdir = tempfile.mkdtemp(prefix='devtool')
1133 update_srcuri = False
1134 try:
1135 local_files_dir = tempfile.mkdtemp(dir=tempdir)
1136 upd_f, new_f, del_f = _export_local_files(srctree, rd, local_files_dir)
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001137 if not no_remove:
Patrick Williamsf1e5d692016-03-30 15:21:19 -05001138 # Find list of existing patches in recipe file
1139 patches_dir = tempfile.mkdtemp(dir=tempdir)
1140 old_srcrev = (rd.getVar('SRCREV', False) or '')
1141 upd_p, new_p, del_p = _export_patches(srctree, rd, old_srcrev,
1142 patches_dir)
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001143
Patrick Williamsf1e5d692016-03-30 15:21:19 -05001144 # Remove deleted local files and "overlapping" patches
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001145 remove_files = list(del_f.values()) + list(upd_p.values())
Patrick Williamsf1e5d692016-03-30 15:21:19 -05001146 if remove_files:
1147 removedentries = _remove_file_entries(srcuri, remove_files)[0]
1148 update_srcuri = True
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001149
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001150 if appendlayerdir:
Patrick Williamsf1e5d692016-03-30 15:21:19 -05001151 files = dict((os.path.join(local_files_dir, key), val) for
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001152 key, val in list(upd_f.items()) + list(new_f.items()))
Patrick Williamsf1e5d692016-03-30 15:21:19 -05001153 removevalues = {}
1154 if update_srcuri:
1155 removevalues = {'SRC_URI': removedentries}
1156 patchfields['SRC_URI'] = '\\\n '.join(srcuri)
1157 _, destpath = oe.recipeutils.bbappend_recipe(
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001158 rd, appendlayerdir, files, wildcardver=wildcard_version,
Patrick Williamsf1e5d692016-03-30 15:21:19 -05001159 extralines=patchfields, removevalues=removevalues)
1160 else:
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001161 files_dir = _determine_files_dir(rd)
1162 for basepath, path in upd_f.items():
Patrick Williamsf1e5d692016-03-30 15:21:19 -05001163 logger.info('Updating file %s' % basepath)
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001164 if os.path.isabs(basepath):
1165 # Original file (probably with subdir pointing inside source tree)
1166 # so we do not want to move it, just copy
1167 _copy_file(basepath, path)
1168 else:
1169 _move_file(os.path.join(local_files_dir, basepath), path)
Patrick Williamsf1e5d692016-03-30 15:21:19 -05001170 update_srcuri= True
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001171 for basepath, path in new_f.items():
Patrick Williamsf1e5d692016-03-30 15:21:19 -05001172 logger.info('Adding new file %s' % basepath)
1173 _move_file(os.path.join(local_files_dir, basepath),
1174 os.path.join(files_dir, basepath))
1175 srcuri.append('file://%s' % basepath)
1176 update_srcuri = True
1177 if update_srcuri:
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001178 patchfields['SRC_URI'] = ' '.join(srcuri)
Patrick Williamsf1e5d692016-03-30 15:21:19 -05001179 oe.recipeutils.patch_recipe(rd, recipefile, patchfields)
1180 finally:
1181 shutil.rmtree(tempdir)
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001182 if not 'git://' in orig_src_uri:
1183 logger.info('You will need to update SRC_URI within the recipe to '
1184 'point to a git repository where you have pushed your '
1185 'changes')
1186
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001187 _remove_source_files(appendlayerdir, remove_files, destpath)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001188 return True
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001189
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001190def _update_recipe_patch(recipename, workspace, srctree, rd, appendlayerdir, wildcard_version, no_remove, initial_rev):
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001191 """Implement the 'patch' mode of update-recipe"""
1192 import bb
1193 import oe.recipeutils
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001194
1195 recipefile = rd.getVar('FILE', True)
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001196 append = workspace[recipename]['bbappend']
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001197 if not os.path.exists(append):
1198 raise DevtoolError('unable to find workspace bbappend for recipe %s' %
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001199 recipename)
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001200
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001201 initial_rev, update_rev, changed_revs = _get_patchset_revs(srctree, append, initial_rev)
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001202 if not initial_rev:
1203 raise DevtoolError('Unable to find initial revision - please specify '
1204 'it with --initial-rev')
1205
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001206 tempdir = tempfile.mkdtemp(prefix='devtool')
1207 try:
Patrick Williamsf1e5d692016-03-30 15:21:19 -05001208 local_files_dir = tempfile.mkdtemp(dir=tempdir)
1209 upd_f, new_f, del_f = _export_local_files(srctree, rd, local_files_dir)
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001210
Patrick Williamsf1e5d692016-03-30 15:21:19 -05001211 remove_files = []
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001212 if not no_remove:
Patrick Williamsf1e5d692016-03-30 15:21:19 -05001213 # Get all patches from source tree and check if any should be removed
1214 all_patches_dir = tempfile.mkdtemp(dir=tempdir)
1215 upd_p, new_p, del_p = _export_patches(srctree, rd, initial_rev,
1216 all_patches_dir)
1217 # Remove deleted local files and patches
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001218 remove_files = list(del_f.values()) + list(del_p.values())
Patrick Williamsf1e5d692016-03-30 15:21:19 -05001219
1220 # Get updated patches from source tree
1221 patches_dir = tempfile.mkdtemp(dir=tempdir)
1222 upd_p, new_p, del_p = _export_patches(srctree, rd, update_rev,
1223 patches_dir)
1224 updatefiles = False
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001225 updaterecipe = False
1226 destpath = None
Patrick Williamsf1e5d692016-03-30 15:21:19 -05001227 srcuri = (rd.getVar('SRC_URI', False) or '').split()
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001228 if appendlayerdir:
Patrick Williamsf1e5d692016-03-30 15:21:19 -05001229 files = dict((os.path.join(local_files_dir, key), val) for
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001230 key, val in list(upd_f.items()) + list(new_f.items()))
Patrick Williamsf1e5d692016-03-30 15:21:19 -05001231 files.update(dict((os.path.join(patches_dir, key), val) for
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001232 key, val in list(upd_p.items()) + list(new_p.items())))
Patrick Williamsf1e5d692016-03-30 15:21:19 -05001233 if files or remove_files:
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001234 removevalues = None
Patrick Williamsf1e5d692016-03-30 15:21:19 -05001235 if remove_files:
1236 removedentries, remaining = _remove_file_entries(
1237 srcuri, remove_files)
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001238 if removedentries or remaining:
1239 remaining = ['file://' + os.path.basename(item) for
1240 item in remaining]
1241 removevalues = {'SRC_URI': removedentries + remaining}
1242 _, destpath = oe.recipeutils.bbappend_recipe(
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001243 rd, appendlayerdir, files,
1244 wildcardver=wildcard_version,
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001245 removevalues=removevalues)
1246 else:
Patrick Williamsf1e5d692016-03-30 15:21:19 -05001247 logger.info('No patches or local source files needed updating')
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001248 else:
Patrick Williamsf1e5d692016-03-30 15:21:19 -05001249 # Update existing files
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001250 for basepath, path in upd_f.items():
Patrick Williamsf1e5d692016-03-30 15:21:19 -05001251 logger.info('Updating file %s' % basepath)
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001252 if os.path.isabs(basepath):
1253 # Original file (probably with subdir pointing inside source tree)
1254 # so we do not want to move it, just copy
1255 _copy_file(basepath, path)
1256 else:
1257 _move_file(os.path.join(local_files_dir, basepath), path)
Patrick Williamsf1e5d692016-03-30 15:21:19 -05001258 updatefiles = True
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001259 for basepath, path in upd_p.items():
Patrick Williamsf1e5d692016-03-30 15:21:19 -05001260 patchfn = os.path.join(patches_dir, basepath)
1261 if changed_revs is not None:
1262 # Avoid updating patches that have not actually changed
1263 with open(patchfn, 'r') as f:
1264 firstlineitems = f.readline().split()
1265 if len(firstlineitems) > 1 and len(firstlineitems[1]) == 40:
1266 if not firstlineitems[1] in changed_revs:
1267 continue
1268 logger.info('Updating patch %s' % basepath)
1269 _move_file(patchfn, path)
1270 updatefiles = True
1271 # Add any new files
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001272 files_dir = _determine_files_dir(rd)
1273 for basepath, path in new_f.items():
Patrick Williamsf1e5d692016-03-30 15:21:19 -05001274 logger.info('Adding new file %s' % basepath)
1275 _move_file(os.path.join(local_files_dir, basepath),
1276 os.path.join(files_dir, basepath))
1277 srcuri.append('file://%s' % basepath)
1278 updaterecipe = True
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001279 for basepath, path in new_p.items():
Patrick Williamsf1e5d692016-03-30 15:21:19 -05001280 logger.info('Adding new patch %s' % basepath)
1281 _move_file(os.path.join(patches_dir, basepath),
1282 os.path.join(files_dir, basepath))
1283 srcuri.append('file://%s' % basepath)
1284 updaterecipe = True
1285 # Update recipe, if needed
1286 if _remove_file_entries(srcuri, remove_files)[0]:
1287 updaterecipe = True
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001288 if updaterecipe:
1289 logger.info('Updating recipe %s' % os.path.basename(recipefile))
1290 oe.recipeutils.patch_recipe(rd, recipefile,
1291 {'SRC_URI': ' '.join(srcuri)})
Patrick Williamsf1e5d692016-03-30 15:21:19 -05001292 elif not updatefiles:
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001293 # Neither patches nor recipe were updated
Patrick Williamsf1e5d692016-03-30 15:21:19 -05001294 logger.info('No patches or files need updating')
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001295 return False
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001296 finally:
1297 shutil.rmtree(tempdir)
1298
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001299 _remove_source_files(appendlayerdir, remove_files, destpath)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001300 return True
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001301
1302def _guess_recipe_update_mode(srctree, rdata):
1303 """Guess the recipe update mode to use"""
1304 src_uri = (rdata.getVar('SRC_URI', False) or '').split()
1305 git_uris = [uri for uri in src_uri if uri.startswith('git://')]
1306 if not git_uris:
1307 return 'patch'
1308 # Just use the first URI for now
1309 uri = git_uris[0]
1310 # Check remote branch
1311 params = bb.fetch.decodeurl(uri)[5]
1312 upstr_branch = params['branch'] if 'branch' in params else 'master'
1313 # Check if current branch HEAD is found in upstream branch
1314 stdout, _ = bb.process.run('git rev-parse HEAD', cwd=srctree)
1315 head_rev = stdout.rstrip()
1316 stdout, _ = bb.process.run('git branch -r --contains %s' % head_rev,
1317 cwd=srctree)
1318 remote_brs = [branch.strip() for branch in stdout.splitlines()]
1319 if 'origin/' + upstr_branch in remote_brs:
1320 return 'srcrev'
1321
1322 return 'patch'
1323
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001324def _update_recipe(recipename, workspace, rd, mode, appendlayerdir, wildcard_version, no_remove, initial_rev):
1325 srctree = workspace[recipename]['srctree']
1326 if mode == 'auto':
1327 mode = _guess_recipe_update_mode(srctree, rd)
1328
1329 if mode == 'srcrev':
1330 updated = _update_recipe_srcrev(srctree, rd, appendlayerdir, wildcard_version, no_remove)
1331 elif mode == 'patch':
1332 updated = _update_recipe_patch(recipename, workspace, srctree, rd, appendlayerdir, wildcard_version, no_remove, initial_rev)
1333 else:
1334 raise DevtoolError('update_recipe: invalid mode %s' % mode)
1335 return updated
1336
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001337def update_recipe(args, config, basepath, workspace):
1338 """Entry point for the devtool 'update-recipe' subcommand"""
Patrick Williamsf1e5d692016-03-30 15:21:19 -05001339 check_workspace_recipe(workspace, args.recipename)
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001340
1341 if args.append:
1342 if not os.path.exists(args.append):
1343 raise DevtoolError('bbappend destination layer directory "%s" '
1344 'does not exist' % args.append)
1345 if not os.path.exists(os.path.join(args.append, 'conf', 'layer.conf')):
1346 raise DevtoolError('conf/layer.conf not found in bbappend '
1347 'destination layer "%s"' % args.append)
1348
Patrick Williamsf1e5d692016-03-30 15:21:19 -05001349 tinfoil = setup_tinfoil(basepath=basepath, tracking=True)
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001350 try:
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001351
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001352 rd = parse_recipe(config, tinfoil, args.recipename, True)
1353 if not rd:
1354 return 1
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001355
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001356 updated = _update_recipe(args.recipename, workspace, rd, args.mode, args.append, args.wildcard_version, args.no_remove, args.initial_rev)
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001357
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001358 if updated:
1359 rf = rd.getVar('FILE', True)
1360 if rf.startswith(config.workspace_path):
1361 logger.warn('Recipe file %s has been updated but is inside the workspace - you will need to move it (and any associated files next to it) out to the desired layer before using "devtool reset" in order to keep any changes' % rf)
1362 finally:
1363 tinfoil.shutdown()
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001364
1365 return 0
1366
1367
1368def status(args, config, basepath, workspace):
1369 """Entry point for the devtool 'status' subcommand"""
1370 if workspace:
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001371 for recipe, value in workspace.items():
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001372 recipefile = value['recipefile']
1373 if recipefile:
1374 recipestr = ' (%s)' % recipefile
1375 else:
1376 recipestr = ''
1377 print("%s: %s%s" % (recipe, value['srctree'], recipestr))
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001378 else:
1379 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')
1380 return 0
1381
1382
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001383def _reset(recipes, no_clean, config, basepath, workspace):
1384 """Reset one or more recipes"""
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001385
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001386 if recipes and not no_clean:
Patrick Williamsf1e5d692016-03-30 15:21:19 -05001387 if len(recipes) == 1:
1388 logger.info('Cleaning sysroot for recipe %s...' % recipes[0])
1389 else:
1390 logger.info('Cleaning sysroot for recipes %s...' % ', '.join(recipes))
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001391 # If the recipe file itself was created in the workspace, and
1392 # it uses BBCLASSEXTEND, then we need to also clean the other
1393 # variants
1394 targets = []
1395 for recipe in recipes:
1396 targets.append(recipe)
1397 recipefile = workspace[recipe]['recipefile']
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001398 if recipefile and os.path.exists(recipefile):
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001399 targets.extend(get_bbclassextend_targets(recipefile, recipe))
Patrick Williamsf1e5d692016-03-30 15:21:19 -05001400 try:
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001401 exec_build_env_command(config.init_path, basepath, 'bitbake -c clean %s' % ' '.join(targets))
Patrick Williamsf1e5d692016-03-30 15:21:19 -05001402 except bb.process.ExecutionError as e:
1403 raise DevtoolError('Command \'%s\' failed, output:\n%s\nIf you '
1404 'wish, you may specify -n/--no-clean to '
1405 'skip running this command when resetting' %
1406 (e.command, e.stdout))
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001407
Patrick Williamsf1e5d692016-03-30 15:21:19 -05001408 for pn in recipes:
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001409 _check_preserve(config, pn)
1410
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001411 preservepath = os.path.join(config.workspace_path, 'attic', pn, pn)
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001412 def preservedir(origdir):
1413 if os.path.exists(origdir):
1414 for root, dirs, files in os.walk(origdir):
1415 for fn in files:
1416 logger.warn('Preserving %s in %s' % (fn, preservepath))
Patrick Williamsf1e5d692016-03-30 15:21:19 -05001417 _move_file(os.path.join(origdir, fn),
1418 os.path.join(preservepath, fn))
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001419 for dn in dirs:
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001420 preservedir(os.path.join(root, dn))
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001421 os.rmdir(origdir)
1422
1423 preservedir(os.path.join(config.workspace_path, 'recipes', pn))
1424 # We don't automatically create this dir next to appends, but the user can
1425 preservedir(os.path.join(config.workspace_path, 'appends', pn))
1426
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001427 srctree = workspace[pn]['srctree']
1428 if os.path.isdir(srctree):
1429 if os.listdir(srctree):
1430 # We don't want to risk wiping out any work in progress
1431 logger.info('Leaving source tree %s as-is; if you no '
1432 'longer need it then please delete it manually'
1433 % srctree)
1434 else:
1435 # This is unlikely, but if it's empty we can just remove it
1436 os.rmdir(srctree)
1437
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001438
1439def reset(args, config, basepath, workspace):
1440 """Entry point for the devtool 'reset' subcommand"""
1441 import bb
1442 if args.recipename:
1443 if args.all:
1444 raise DevtoolError("Recipe cannot be specified if -a/--all is used")
1445 else:
1446 for recipe in args.recipename:
1447 check_workspace_recipe(workspace, recipe, checksrc=False)
1448 elif not args.all:
1449 raise DevtoolError("Recipe must be specified, or specify -a/--all to "
1450 "reset all recipes")
1451 if args.all:
1452 recipes = list(workspace.keys())
1453 else:
1454 recipes = args.recipename
1455
1456 _reset(recipes, args.no_clean, config, basepath, workspace)
1457
1458 return 0
1459
1460
1461def _get_layer(layername, d):
1462 """Determine the base layer path for the specified layer name/path"""
1463 layerdirs = d.getVar('BBLAYERS', True).split()
1464 layers = {os.path.basename(p): p for p in layerdirs}
1465 # Provide some shortcuts
1466 if layername.lower() in ['oe-core', 'openembedded-core']:
1467 layerdir = layers.get('meta', None)
1468 else:
1469 layerdir = layers.get(layername, None)
1470 if layerdir:
1471 layerdir = os.path.abspath(layerdir)
1472 return layerdir or layername
1473
1474def finish(args, config, basepath, workspace):
1475 """Entry point for the devtool 'finish' subcommand"""
1476 import bb
1477 import oe.recipeutils
1478
1479 check_workspace_recipe(workspace, args.recipename)
1480
1481 tinfoil = setup_tinfoil(basepath=basepath, tracking=True)
1482 try:
1483 rd = parse_recipe(config, tinfoil, args.recipename, True)
1484 if not rd:
1485 return 1
1486
1487 destlayerdir = _get_layer(args.destination, tinfoil.config_data)
1488 origlayerdir = oe.recipeutils.find_layerdir(rd.getVar('FILE', True))
1489
1490 if not os.path.isdir(destlayerdir):
1491 raise DevtoolError('Unable to find layer or directory matching "%s"' % args.destination)
1492
1493 if os.path.abspath(destlayerdir) == config.workspace_path:
1494 raise DevtoolError('"%s" specifies the workspace layer - that is not a valid destination' % args.destination)
1495
1496 # If it's an upgrade, grab the original path
1497 origpath = None
1498 origfilelist = None
1499 append = workspace[args.recipename]['bbappend']
1500 with open(append, 'r') as f:
1501 for line in f:
1502 if line.startswith('# original_path:'):
1503 origpath = line.split(':')[1].strip()
1504 elif line.startswith('# original_files:'):
1505 origfilelist = line.split(':')[1].split()
1506
1507 if origlayerdir == config.workspace_path:
1508 # Recipe file itself is in workspace, update it there first
1509 appendlayerdir = None
1510 origrelpath = None
1511 if origpath:
1512 origlayerpath = oe.recipeutils.find_layerdir(origpath)
1513 if origlayerpath:
1514 origrelpath = os.path.relpath(origpath, origlayerpath)
1515 destpath = oe.recipeutils.get_bbfile_path(rd, destlayerdir, origrelpath)
1516 if not destpath:
1517 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))
1518 elif destlayerdir == origlayerdir:
1519 # Same layer, update the original recipe
1520 appendlayerdir = None
1521 destpath = None
1522 else:
1523 # Create/update a bbappend in the specified layer
1524 appendlayerdir = destlayerdir
1525 destpath = None
1526
1527 # Remove any old files in the case of an upgrade
1528 if origpath and origfilelist and oe.recipeutils.find_layerdir(origpath) == oe.recipeutils.find_layerdir(destlayerdir):
1529 for fn in origfilelist:
1530 fnp = os.path.join(origpath, fn)
1531 try:
1532 os.remove(fnp)
1533 except FileNotFoundError:
1534 pass
1535
1536 # Actually update the recipe / bbappend
1537 _update_recipe(args.recipename, workspace, rd, args.mode, appendlayerdir, wildcard_version=True, no_remove=False, initial_rev=args.initial_rev)
1538
1539 if origlayerdir == config.workspace_path and destpath:
1540 # Recipe file itself is in the workspace - need to move it and any
1541 # associated files to the specified layer
1542 logger.info('Moving recipe file to %s' % destpath)
1543 recipedir = os.path.dirname(rd.getVar('FILE', True))
1544 for root, _, files in os.walk(recipedir):
1545 for fn in files:
1546 srcpath = os.path.join(root, fn)
1547 relpth = os.path.relpath(os.path.dirname(srcpath), recipedir)
1548 destdir = os.path.abspath(os.path.join(destpath, relpth))
1549 bb.utils.mkdirhier(destdir)
1550 shutil.move(srcpath, os.path.join(destdir, fn))
1551
1552 finally:
1553 tinfoil.shutdown()
1554
1555 # Everything else has succeeded, we can now reset
1556 _reset([args.recipename], no_clean=False, config=config, basepath=basepath, workspace=workspace)
1557
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001558 return 0
1559
1560
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001561def get_default_srctree(config, recipename=''):
1562 """Get the default srctree path"""
1563 srctreeparent = config.get('General', 'default_source_parent_dir', config.workspace_path)
1564 if recipename:
1565 return os.path.join(srctreeparent, 'sources', recipename)
1566 else:
1567 return os.path.join(srctreeparent, 'sources')
1568
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001569def register_commands(subparsers, context):
1570 """Register devtool subcommands from this plugin"""
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001571
1572 defsrctree = get_default_srctree(context.config)
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001573 parser_add = subparsers.add_parser('add', help='Add a new recipe',
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001574 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.',
1575 group='starting', order=100)
1576 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.')
1577 parser_add.add_argument('srctree', nargs='?', help='Path to external source tree. If not specified, a subdirectory of %s will be used.' % defsrctree)
1578 parser_add.add_argument('fetchuri', nargs='?', help='Fetch the specified URI and extract it to create the source tree')
Patrick Williamsf1e5d692016-03-30 15:21:19 -05001579 group = parser_add.add_mutually_exclusive_group()
1580 group.add_argument('--same-dir', '-s', help='Build in same directory as source', action="store_true")
1581 group.add_argument('--no-same-dir', help='Force build in a separate build directory', action="store_true")
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001582 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')
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001583 parser_add.add_argument('--version', '-V', help='Version to use within recipe (PV)')
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001584 parser_add.add_argument('--no-git', '-g', help='If fetching source, do not set up source tree as a git repository', action="store_true")
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001585 parser_add.add_argument('--autorev', '-a', help='When fetching from a git repository, set SRCREV in the recipe to a floating revision instead of fixed', action="store_true")
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001586 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')
1587 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')
1588 parser_add.add_argument('--src-subdir', help='Specify subdirectory within source tree to use', metavar='SUBDIR')
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001589 parser_add.set_defaults(func=add, fixed_setup=context.fixed_setup)
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001590
1591 parser_modify = subparsers.add_parser('modify', help='Modify the source for an existing recipe',
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001592 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.',
1593 group='starting', order=90)
1594 parser_modify.add_argument('recipename', help='Name of existing recipe to edit (just name - no version, path or extension)')
1595 parser_modify.add_argument('srctree', nargs='?', help='Path to external source tree. If not specified, a subdirectory of %s will be used.' % defsrctree)
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001596 parser_modify.add_argument('--wildcard', '-w', action="store_true", help='Use wildcard for unversioned bbappend')
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001597 group = parser_modify.add_mutually_exclusive_group()
1598 group.add_argument('--extract', '-x', action="store_true", help='Extract source for recipe (default)')
1599 group.add_argument('--no-extract', '-n', action="store_true", help='Do not extract source, expect it to exist')
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001600 group = parser_modify.add_mutually_exclusive_group()
1601 group.add_argument('--same-dir', '-s', help='Build in same directory as source', action="store_true")
1602 group.add_argument('--no-same-dir', help='Force build in a separate build directory', action="store_true")
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001603 parser_modify.add_argument('--branch', '-b', default="devtool", help='Name for development branch to checkout (when not using -n/--no-extract) (default "%(default)s")')
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001604 parser_modify.set_defaults(func=modify)
1605
1606 parser_extract = subparsers.add_parser('extract', help='Extract the source for an existing recipe',
1607 description='Extracts the source for an existing recipe',
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001608 group='advanced')
1609 parser_extract.add_argument('recipename', help='Name of recipe to extract the source for')
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001610 parser_extract.add_argument('srctree', help='Path to where to extract the source tree')
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001611 parser_extract.add_argument('--branch', '-b', default="devtool", help='Name for development branch to checkout (default "%(default)s")')
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001612 parser_extract.add_argument('--keep-temp', action="store_true", help='Keep temporary directory (for debugging)')
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001613 parser_extract.set_defaults(func=extract, no_workspace=True)
1614
1615 parser_sync = subparsers.add_parser('sync', help='Synchronize the source tree for an existing recipe',
1616 description='Synchronize the previously extracted source tree for an existing recipe',
1617 formatter_class=argparse.ArgumentDefaultsHelpFormatter,
1618 group='advanced')
1619 parser_sync.add_argument('recipename', help='Name of recipe to sync the source for')
1620 parser_sync.add_argument('srctree', help='Path to the source tree')
1621 parser_sync.add_argument('--branch', '-b', default="devtool", help='Name for development branch to checkout')
1622 parser_sync.add_argument('--keep-temp', action="store_true", help='Keep temporary directory (for debugging)')
1623 parser_sync.set_defaults(func=sync)
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001624
1625 parser_update_recipe = subparsers.add_parser('update-recipe', help='Apply changes from external source tree to recipe',
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001626 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.',
1627 group='working', order=-90)
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001628 parser_update_recipe.add_argument('recipename', help='Name of recipe to update')
1629 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')
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001630 parser_update_recipe.add_argument('--initial-rev', help='Override starting revision for patches')
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001631 parser_update_recipe.add_argument('--append', '-a', help='Write changes to a bbappend in the specified layer instead of the recipe', metavar='LAYERDIR')
1632 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')
1633 parser_update_recipe.add_argument('--no-remove', '-n', action="store_true", help='Don\'t remove patches, only add or update')
1634 parser_update_recipe.set_defaults(func=update_recipe)
1635
1636 parser_status = subparsers.add_parser('status', help='Show workspace status',
1637 description='Lists recipes currently in your workspace and the paths to their respective external source trees',
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001638 group='info', order=100)
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001639 parser_status.set_defaults(func=status)
1640
1641 parser_reset = subparsers.add_parser('reset', help='Remove a recipe from your workspace',
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001642 description='Removes the specified recipe(s) from your workspace (resetting its state back to that defined by the metadata).',
Patrick Williamsd8c66bc2016-06-20 12:57:21 -05001643 group='working', order=-100)
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001644 parser_reset.add_argument('recipename', nargs='*', help='Recipe to reset')
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001645 parser_reset.add_argument('--all', '-a', action="store_true", help='Reset all recipes (clear workspace)')
1646 parser_reset.add_argument('--no-clean', '-n', action="store_true", help='Don\'t clean the sysroot to remove recipe output')
1647 parser_reset.set_defaults(func=reset)
Patrick Williamsc0f7c042017-02-23 20:41:17 -06001648
1649 parser_finish = subparsers.add_parser('finish', help='Finish working on a recipe in your workspace',
1650 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.',
1651 group='working', order=-100)
1652 parser_finish.add_argument('recipename', help='Recipe to finish')
1653 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.')
1654 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')
1655 parser_finish.add_argument('--initial-rev', help='Override starting revision for patches')
1656 parser_finish.set_defaults(func=finish)