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