blob: d5900b4f8279008f0b0a963ee3e0041a693544e8 [file] [log] [blame]
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001# Development tool - standard commands plugin
2#
3# Copyright (C) 2014-2015 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 tempfile
24import logging
25import argparse
26import scriptutils
27import errno
28from devtool import exec_build_env_command, setup_tinfoil, DevtoolError
29from devtool import parse_recipe
30
31logger = logging.getLogger('devtool')
32
33
34def add(args, config, basepath, workspace):
35 """Entry point for the devtool 'add' subcommand"""
36 import bb
37 import oe.recipeutils
38
39 if args.recipename in workspace:
40 raise DevtoolError("recipe %s is already in your workspace" %
41 args.recipename)
42
43 reason = oe.recipeutils.validate_pn(args.recipename)
44 if reason:
45 raise DevtoolError(reason)
46
47 srctree = os.path.abspath(args.srctree)
48 if os.path.exists(srctree):
49 if args.fetch:
50 if not os.path.isdir(srctree):
51 raise DevtoolError("Cannot fetch into source tree path %s as "
52 "it exists and is not a directory" %
53 srctree)
54 elif os.listdir(srctree):
55 raise DevtoolError("Cannot fetch into source tree path %s as "
56 "it already exists and is non-empty" %
57 srctree)
58 elif not args.fetch:
59 raise DevtoolError("Specified source tree %s could not be found" %
60 srctree)
61
62 appendpath = os.path.join(config.workspace_path, 'appends')
63 if not os.path.exists(appendpath):
64 os.makedirs(appendpath)
65
66 recipedir = os.path.join(config.workspace_path, 'recipes', args.recipename)
67 bb.utils.mkdirhier(recipedir)
68 rfv = None
69 if args.version:
70 if '_' in args.version or ' ' in args.version:
71 raise DevtoolError('Invalid version string "%s"' % args.version)
72 rfv = args.version
73 if args.fetch:
74 if args.fetch.startswith('git://'):
75 rfv = 'git'
76 elif args.fetch.startswith('svn://'):
77 rfv = 'svn'
78 elif args.fetch.startswith('hg://'):
79 rfv = 'hg'
80 if rfv:
81 bp = "%s_%s" % (args.recipename, rfv)
82 else:
83 bp = args.recipename
84 recipefile = os.path.join(recipedir, "%s.bb" % bp)
85 if sys.stdout.isatty():
86 color = 'always'
87 else:
88 color = args.color
89 extracmdopts = ''
90 if args.fetch:
91 source = args.fetch
92 extracmdopts = '-x %s' % srctree
93 else:
94 source = srctree
95 if args.version:
96 extracmdopts += ' -V %s' % args.version
97 try:
98 stdout, _ = exec_build_env_command(config.init_path, basepath, 'recipetool --color=%s create -o %s "%s" %s' % (color, recipefile, source, extracmdopts))
99 logger.info('Recipe %s has been automatically created; further editing may be required to make it fully functional' % recipefile)
100 except bb.process.ExecutionError as e:
101 raise DevtoolError('Command \'%s\' failed:\n%s' % (e.command, e.stdout))
102
103 _add_md5(config, args.recipename, recipefile)
104
105 initial_rev = None
106 if os.path.exists(os.path.join(srctree, '.git')):
107 (stdout, _) = bb.process.run('git rev-parse HEAD', cwd=srctree)
108 initial_rev = stdout.rstrip()
109
110 appendfile = os.path.join(appendpath, '%s.bbappend' % bp)
111 with open(appendfile, 'w') as f:
112 f.write('inherit externalsrc\n')
113 f.write('EXTERNALSRC = "%s"\n' % srctree)
114 if args.same_dir:
115 f.write('EXTERNALSRC_BUILD = "%s"\n' % srctree)
116 if initial_rev:
117 f.write('\n# initial_rev: %s\n' % initial_rev)
118
119 _add_md5(config, args.recipename, appendfile)
120
121 return 0
122
123
124def _check_compatible_recipe(pn, d):
125 """Check if the recipe is supported by devtool"""
126 if pn == 'perf':
127 raise DevtoolError("The perf recipe does not actually check out "
128 "source and thus cannot be supported by this tool")
129
130 if pn in ['kernel-devsrc', 'package-index'] or pn.startswith('gcc-source'):
131 raise DevtoolError("The %s recipe is not supported by this tool" % pn)
132
133 if bb.data.inherits_class('image', d):
134 raise DevtoolError("The %s recipe is an image, and therefore is not "
135 "supported by this tool" % pn)
136
137 if bb.data.inherits_class('populate_sdk', d):
138 raise DevtoolError("The %s recipe is an SDK, and therefore is not "
139 "supported by this tool" % pn)
140
141 if bb.data.inherits_class('packagegroup', d):
142 raise DevtoolError("The %s recipe is a packagegroup, and therefore is "
143 "not supported by this tool" % pn)
144
145 if bb.data.inherits_class('meta', d):
146 raise DevtoolError("The %s recipe is a meta-recipe, and therefore is "
147 "not supported by this tool" % pn)
148
149 if bb.data.inherits_class('externalsrc', d) and d.getVar('EXTERNALSRC', True):
150 raise DevtoolError("externalsrc is currently enabled for the %s "
151 "recipe. This prevents the normal do_patch task "
152 "from working. You will need to disable this "
153 "first." % pn)
154
155def _ls_tree(directory):
156 """Recursive listing of files in a directory"""
157 ret = []
158 for root, dirs, files in os.walk(directory):
159 ret.extend([os.path.relpath(os.path.join(root, fname), directory) for
160 fname in files])
161 return ret
162
163
164def extract(args, config, basepath, workspace):
165 """Entry point for the devtool 'extract' subcommand"""
166 import bb
167
168 tinfoil = _prep_extract_operation(config, basepath, args.recipename)
169
170 rd = parse_recipe(config, tinfoil, args.recipename, True)
171 if not rd:
172 return 1
173
174 srctree = os.path.abspath(args.srctree)
175 initial_rev = _extract_source(srctree, args.keep_temp, args.branch, rd)
176 logger.info('Source tree extracted to %s' % srctree)
177
178 if initial_rev:
179 return 0
180 else:
181 return 1
182
183class BbTaskExecutor(object):
184 """Class for executing bitbake tasks for a recipe
185
186 FIXME: This is very awkward. Unfortunately it's not currently easy to
187 properly execute tasks outside of bitbake itself, until then this has to
188 suffice if we are to handle e.g. linux-yocto's extra tasks
189 """
190
191 def __init__(self, rdata):
192 self.rdata = rdata
193 self.executed = []
194
195 def exec_func(self, func, report):
196 """Run bitbake task function"""
197 if not func in self.executed:
198 deps = self.rdata.getVarFlag(func, 'deps')
199 if deps:
200 for taskdepfunc in deps:
201 self.exec_func(taskdepfunc, True)
202 if report:
203 logger.info('Executing %s...' % func)
204 fn = self.rdata.getVar('FILE', True)
205 localdata = bb.build._task_data(fn, func, self.rdata)
206 bb.build.exec_func(func, localdata)
207 self.executed.append(func)
208
209
210def _prep_extract_operation(config, basepath, recipename):
211 """HACK: Ugly workaround for making sure that requirements are met when
212 trying to extract a package. Returns the tinfoil instance to be used."""
213 tinfoil = setup_tinfoil()
214 rd = parse_recipe(config, tinfoil, recipename, True)
215
216 if bb.data.inherits_class('kernel-yocto', rd):
217 tinfoil.shutdown()
218 try:
219 stdout, _ = exec_build_env_command(config.init_path, basepath,
220 'bitbake kern-tools-native')
221 tinfoil = setup_tinfoil()
222 except bb.process.ExecutionError as err:
223 raise DevtoolError("Failed to build kern-tools-native:\n%s" %
224 err.stdout)
225 return tinfoil
226
227
228def _extract_source(srctree, keep_temp, devbranch, d):
229 """Extract sources of a recipe"""
230 import bb.event
231 import oe.recipeutils
232
233 def eventfilter(name, handler, event, d):
234 """Bitbake event filter for devtool extract operation"""
235 if name == 'base_eventhandler':
236 return True
237 else:
238 return False
239
240 if hasattr(bb.event, 'set_eventfilter'):
241 bb.event.set_eventfilter(eventfilter)
242
243 pn = d.getVar('PN', True)
244
245 _check_compatible_recipe(pn, d)
246
247 if os.path.exists(srctree):
248 if not os.path.isdir(srctree):
249 raise DevtoolError("output path %s exists and is not a directory" %
250 srctree)
251 elif os.listdir(srctree):
252 raise DevtoolError("output path %s already exists and is "
253 "non-empty" % srctree)
254
255 # Prepare for shutil.move later on
256 bb.utils.mkdirhier(srctree)
257 os.rmdir(srctree)
258
259 # We don't want notes to be printed, they are too verbose
260 origlevel = bb.logger.getEffectiveLevel()
261 if logger.getEffectiveLevel() > logging.DEBUG:
262 bb.logger.setLevel(logging.WARNING)
263
264 initial_rev = None
265 tempdir = tempfile.mkdtemp(prefix='devtool')
266 try:
267 crd = d.createCopy()
268 # Make a subdir so we guard against WORKDIR==S
269 workdir = os.path.join(tempdir, 'workdir')
270 crd.setVar('WORKDIR', workdir)
271 crd.setVar('T', os.path.join(tempdir, 'temp'))
272 if not crd.getVar('S', True).startswith(workdir):
273 # Usually a shared workdir recipe (kernel, gcc)
274 # Try to set a reasonable default
275 if bb.data.inherits_class('kernel', d):
276 crd.setVar('S', '${WORKDIR}/source')
277 else:
278 crd.setVar('S', '${WORKDIR}/${BP}')
279 if bb.data.inherits_class('kernel', d):
280 # We don't want to move the source to STAGING_KERNEL_DIR here
281 crd.setVar('STAGING_KERNEL_DIR', '${S}')
282
283 task_executor = BbTaskExecutor(crd)
284
285 crd.setVar('EXTERNALSRC_forcevariable', '')
286
287 logger.info('Fetching %s...' % pn)
288 task_executor.exec_func('do_fetch', False)
289 logger.info('Unpacking...')
290 task_executor.exec_func('do_unpack', False)
291 if bb.data.inherits_class('kernel-yocto', d):
292 # Extra step for kernel to populate the source directory
293 logger.info('Doing kernel checkout...')
294 task_executor.exec_func('do_kernel_checkout', False)
295 srcsubdir = crd.getVar('S', True)
296 if srcsubdir == workdir:
297 # Find non-patch sources that were "unpacked" to srctree directory
298 recipe_patches = [os.path.basename(patch) for patch in
299 oe.recipeutils.get_recipe_patches(crd)]
300 src_files = [fname for fname in _ls_tree(workdir) if
301 os.path.basename(fname) not in recipe_patches]
302 # Force separate S so that patch files can be left out from srctree
303 srcsubdir = tempfile.mkdtemp(dir=workdir)
304 crd.setVar('S', srcsubdir)
305 # Move source files to S
306 for path in src_files:
307 tgt_dir = os.path.join(srcsubdir, os.path.dirname(path))
308 bb.utils.mkdirhier(tgt_dir)
309 shutil.move(os.path.join(workdir, path), tgt_dir)
310 elif os.path.dirname(srcsubdir) != workdir:
311 # Handle if S is set to a subdirectory of the source
312 srcsubdir = os.path.join(workdir, os.path.relpath(srcsubdir, workdir).split(os.sep)[0])
313
314 scriptutils.git_convert_standalone_clone(srcsubdir)
315
316 patchdir = os.path.join(srcsubdir, 'patches')
317 haspatches = False
318 if os.path.exists(patchdir):
319 if os.listdir(patchdir):
320 haspatches = True
321 else:
322 os.rmdir(patchdir)
323
324 if not os.listdir(srcsubdir):
325 raise DevtoolError("no source unpacked to S, perhaps the %s "
326 "recipe doesn't use any source?" % pn)
327
328 if not os.path.exists(os.path.join(srcsubdir, '.git')):
329 bb.process.run('git init', cwd=srcsubdir)
330 bb.process.run('git add .', cwd=srcsubdir)
331 bb.process.run('git commit -q -m "Initial commit from upstream at version %s"' % crd.getVar('PV', True), cwd=srcsubdir)
332
333 (stdout, _) = bb.process.run('git rev-parse HEAD', cwd=srcsubdir)
334 initial_rev = stdout.rstrip()
335
336 bb.process.run('git checkout -b %s' % devbranch, cwd=srcsubdir)
337 bb.process.run('git tag -f devtool-base', cwd=srcsubdir)
338 crd.setVar('PATCHTOOL', 'git')
339
340 logger.info('Patching...')
341 task_executor.exec_func('do_patch', False)
342
343 bb.process.run('git tag -f devtool-patched', cwd=srcsubdir)
344
345 if os.path.exists(patchdir):
346 shutil.rmtree(patchdir)
347 if haspatches:
348 bb.process.run('git checkout patches', cwd=srcsubdir)
349
350 shutil.move(srcsubdir, srctree)
351 finally:
352 bb.logger.setLevel(origlevel)
353
354 if keep_temp:
355 logger.info('Preserving temporary directory %s' % tempdir)
356 else:
357 shutil.rmtree(tempdir)
358 return initial_rev
359
360def _add_md5(config, recipename, filename):
361 """Record checksum of a file (or recursively for a directory) to the md5-file of the workspace"""
362 import bb.utils
363
364 def addfile(fn):
365 md5 = bb.utils.md5_file(fn)
366 with open(os.path.join(config.workspace_path, '.devtool_md5'), 'a') as f:
367 f.write('%s|%s|%s\n' % (recipename, os.path.relpath(fn, config.workspace_path), md5))
368
369 if os.path.isdir(filename):
370 for root, _, files in os.walk(os.path.dirname(filename)):
371 for f in files:
372 addfile(os.path.join(root, f))
373 else:
374 addfile(filename)
375
376def _check_preserve(config, recipename):
377 """Check if a file was manually changed and needs to be saved in 'attic'
378 directory"""
379 import bb.utils
380 origfile = os.path.join(config.workspace_path, '.devtool_md5')
381 newfile = os.path.join(config.workspace_path, '.devtool_md5_new')
382 preservepath = os.path.join(config.workspace_path, 'attic')
383 with open(origfile, 'r') as f:
384 with open(newfile, 'w') as tf:
385 for line in f.readlines():
386 splitline = line.rstrip().split('|')
387 if splitline[0] == recipename:
388 removefile = os.path.join(config.workspace_path, splitline[1])
389 try:
390 md5 = bb.utils.md5_file(removefile)
391 except IOError as err:
392 if err.errno == 2:
393 # File no longer exists, skip it
394 continue
395 else:
396 raise
397 if splitline[2] != md5:
398 bb.utils.mkdirhier(preservepath)
399 preservefile = os.path.basename(removefile)
400 logger.warn('File %s modified since it was written, preserving in %s' % (preservefile, preservepath))
401 shutil.move(removefile, os.path.join(preservepath, preservefile))
402 else:
403 os.remove(removefile)
404 else:
405 tf.write(line)
406 os.rename(newfile, origfile)
407
408def modify(args, config, basepath, workspace):
409 """Entry point for the devtool 'modify' subcommand"""
410 import bb
411 import oe.recipeutils
412
413 if args.recipename in workspace:
414 raise DevtoolError("recipe %s is already in your workspace" %
415 args.recipename)
416
417 if not args.extract and not os.path.isdir(args.srctree):
418 raise DevtoolError("directory %s does not exist or not a directory "
419 "(specify -x to extract source from recipe)" %
420 args.srctree)
421 if args.extract:
422 tinfoil = _prep_extract_operation(config, basepath, args.recipename)
423 else:
424 tinfoil = setup_tinfoil()
425
426 rd = parse_recipe(config, tinfoil, args.recipename, True)
427 if not rd:
428 return 1
429 recipefile = rd.getVar('FILE', True)
430 appendname = os.path.splitext(os.path.basename(recipefile))[0]
431 if args.wildcard:
432 appendname = re.sub(r'_.*', '_%', appendname)
433 appendpath = os.path.join(config.workspace_path, 'appends')
434 appendfile = os.path.join(appendpath, appendname + '.bbappend')
435 if os.path.exists(appendfile):
436 raise DevtoolError("Another variant of recipe %s is already in your "
437 "workspace (only one variant of a recipe can "
438 "currently be worked on at once)"
439 % args.recipename)
440
441 _check_compatible_recipe(args.recipename, rd)
442
443 initial_rev = None
444 commits = []
445 srctree = os.path.abspath(args.srctree)
446 if args.extract:
447 initial_rev = _extract_source(args.srctree, False, args.branch, rd)
448 if not initial_rev:
449 return 1
450 logger.info('Source tree extracted to %s' % srctree)
451 # Get list of commits since this revision
452 (stdout, _) = bb.process.run('git rev-list --reverse %s..HEAD' % initial_rev, cwd=args.srctree)
453 commits = stdout.split()
454 else:
455 if os.path.exists(os.path.join(args.srctree, '.git')):
456 # Check if it's a tree previously extracted by us
457 try:
458 (stdout, _) = bb.process.run('git branch --contains devtool-base', cwd=args.srctree)
459 except bb.process.ExecutionError:
460 stdout = ''
461 for line in stdout.splitlines():
462 if line.startswith('*'):
463 (stdout, _) = bb.process.run('git rev-parse devtool-base', cwd=args.srctree)
464 initial_rev = stdout.rstrip()
465 if not initial_rev:
466 # Otherwise, just grab the head revision
467 (stdout, _) = bb.process.run('git rev-parse HEAD', cwd=args.srctree)
468 initial_rev = stdout.rstrip()
469
470 # Check that recipe isn't using a shared workdir
471 s = os.path.abspath(rd.getVar('S', True))
472 workdir = os.path.abspath(rd.getVar('WORKDIR', True))
473 if s.startswith(workdir) and s != workdir and os.path.dirname(s) != workdir:
474 # Handle if S is set to a subdirectory of the source
475 srcsubdir = os.path.relpath(s, workdir).split(os.sep, 1)[1]
476 srctree = os.path.join(srctree, srcsubdir)
477
478 if not os.path.exists(appendpath):
479 os.makedirs(appendpath)
480 with open(appendfile, 'w') as f:
481 f.write('FILESEXTRAPATHS_prepend := "${THISDIR}/${PN}:"\n\n')
482 f.write('inherit externalsrc\n')
483 f.write('# NOTE: We use pn- overrides here to avoid affecting multiple variants in the case where the recipe uses BBCLASSEXTEND\n')
484 f.write('EXTERNALSRC_pn-%s = "%s"\n' % (args.recipename, srctree))
485
486 b_is_s = True
487 if args.no_same_dir:
488 logger.info('using separate build directory since --no-same-dir specified')
489 b_is_s = False
490 elif args.same_dir:
491 logger.info('using source tree as build directory since --same-dir specified')
492 elif bb.data.inherits_class('autotools-brokensep', rd):
493 logger.info('using source tree as build directory since original recipe inherits autotools-brokensep')
494 elif rd.getVar('B', True) == s:
495 logger.info('using source tree as build directory since that is the default for this recipe')
496 else:
497 b_is_s = False
498 if b_is_s:
499 f.write('EXTERNALSRC_BUILD_pn-%s = "%s"\n' % (args.recipename, srctree))
500
501 if bb.data.inherits_class('kernel', rd):
502 f.write('SRCTREECOVEREDTASKS = "do_validate_branches do_kernel_checkout do_fetch do_unpack"\n')
503 if initial_rev:
504 f.write('\n# initial_rev: %s\n' % initial_rev)
505 for commit in commits:
506 f.write('# commit: %s\n' % commit)
507
508 _add_md5(config, args.recipename, appendfile)
509
510 logger.info('Recipe %s now set up to build from %s' % (args.recipename, srctree))
511
512 return 0
513
514def _get_patchset_revs(args, srctree, recipe_path):
515 """Get initial and update rev of a recipe. These are the start point of the
516 whole patchset and start point for the patches to be re-generated/updated.
517 """
518 import bb
519
520 if args.initial_rev:
521 return args.initial_rev, args.initial_rev
522
523 # Parse initial rev from recipe
524 commits = []
525 initial_rev = None
526 with open(recipe_path, 'r') as f:
527 for line in f:
528 if line.startswith('# initial_rev:'):
529 initial_rev = line.split(':')[-1].strip()
530 elif line.startswith('# commit:'):
531 commits.append(line.split(':')[-1].strip())
532
533 update_rev = initial_rev
534 if initial_rev:
535 # Find first actually changed revision
536 stdout, _ = bb.process.run('git rev-list --reverse %s..HEAD' %
537 initial_rev, cwd=srctree)
538 newcommits = stdout.split()
539 for i in xrange(min(len(commits), len(newcommits))):
540 if newcommits[i] == commits[i]:
541 update_rev = commits[i]
542
543 return initial_rev, update_rev
544
545def _remove_patch_entries(srcuri, patchlist):
546 """Remove patch entries from SRC_URI"""
547 remaining = patchlist[:]
548 entries = []
549 for patch in patchlist:
550 patchfile = os.path.basename(patch)
551 for i in xrange(len(srcuri)):
552 if srcuri[i].startswith('file://') and os.path.basename(srcuri[i].split(';')[0]) == patchfile:
553 entries.append(srcuri[i])
554 remaining.remove(patch)
555 srcuri.pop(i)
556 break
557 return entries, remaining
558
559def _remove_patch_files(args, patches, destpath):
560 """Unlink existing patch files"""
561 for patchfile in patches:
562 if args.append:
563 if not destpath:
564 raise Exception('destpath should be set here')
565 patchfile = os.path.join(destpath, os.path.basename(patchfile))
566
567 if os.path.exists(patchfile):
568 logger.info('Removing patch %s' % patchfile)
569 # FIXME "git rm" here would be nice if the file in question is
570 # tracked
571 # FIXME there's a chance that this file is referred to by
572 # another recipe, in which case deleting wouldn't be the
573 # right thing to do
574 os.remove(patchfile)
575 # Remove directory if empty
576 try:
577 os.rmdir(os.path.dirname(patchfile))
578 except OSError as ose:
579 if ose.errno != errno.ENOTEMPTY:
580 raise
581
582def _update_recipe_srcrev(args, srctree, rd, config_data):
583 """Implement the 'srcrev' mode of update-recipe"""
584 import bb
585 import oe.recipeutils
586 from oe.patch import GitApplyTree
587
588 recipefile = rd.getVar('FILE', True)
589 logger.info('Updating SRCREV in recipe %s' % os.path.basename(recipefile))
590
591 # Get HEAD revision
592 try:
593 stdout, _ = bb.process.run('git rev-parse HEAD', cwd=srctree)
594 except bb.process.ExecutionError as err:
595 raise DevtoolError('Failed to get HEAD revision in %s: %s' %
596 (srctree, err))
597 srcrev = stdout.strip()
598 if len(srcrev) != 40:
599 raise DevtoolError('Invalid hash returned by git: %s' % stdout)
600
601 destpath = None
602 removepatches = []
603 patchfields = {}
604 patchfields['SRCREV'] = srcrev
605 orig_src_uri = rd.getVar('SRC_URI', False) or ''
606 if not args.no_remove:
607 # Find list of existing patches in recipe file
608 existing_patches = oe.recipeutils.get_recipe_patches(rd)
609
610 old_srcrev = (rd.getVar('SRCREV', False) or '')
611 tempdir = tempfile.mkdtemp(prefix='devtool')
612 try:
613 GitApplyTree.extractPatches(srctree, old_srcrev, tempdir)
614 newpatches = os.listdir(tempdir)
615 for patch in existing_patches:
616 patchfile = os.path.basename(patch)
617 if patchfile in newpatches:
618 removepatches.append(patch)
619 finally:
620 shutil.rmtree(tempdir)
621
622 if removepatches:
623 srcuri = orig_src_uri.split()
624 removedentries, _ = _remove_patch_entries(srcuri, removepatches)
625 if removedentries:
626 patchfields['SRC_URI'] = ' '.join(srcuri)
627
628 if args.append:
629 _, destpath = oe.recipeutils.bbappend_recipe(
630 rd, args.append, None, wildcardver=args.wildcard_version,
631 extralines=patchfields)
632 else:
633 oe.recipeutils.patch_recipe(rd, recipefile, patchfields)
634
635 if not 'git://' in orig_src_uri:
636 logger.info('You will need to update SRC_URI within the recipe to '
637 'point to a git repository where you have pushed your '
638 'changes')
639
640 _remove_patch_files(args, removepatches, destpath)
641
642def _update_recipe_patch(args, config, srctree, rd, config_data):
643 """Implement the 'patch' mode of update-recipe"""
644 import bb
645 import oe.recipeutils
646 from oe.patch import GitApplyTree
647
648 recipefile = rd.getVar('FILE', True)
649 append = os.path.join(config.workspace_path, 'appends', '%s.bbappend' %
650 os.path.splitext(os.path.basename(recipefile))[0])
651 if not os.path.exists(append):
652 raise DevtoolError('unable to find workspace bbappend for recipe %s' %
653 args.recipename)
654
655 initial_rev, update_rev = _get_patchset_revs(args, srctree, append)
656 if not initial_rev:
657 raise DevtoolError('Unable to find initial revision - please specify '
658 'it with --initial-rev')
659
660 # Find list of existing patches in recipe file
661 existing_patches = oe.recipeutils.get_recipe_patches(rd)
662
663 removepatches = []
664 seqpatch_re = re.compile('^([0-9]{4}-)?(.+)')
665 if not args.no_remove:
666 # Get all patches from source tree and check if any should be removed
667 tempdir = tempfile.mkdtemp(prefix='devtool')
668 try:
669 GitApplyTree.extractPatches(srctree, initial_rev, tempdir)
670 # Strip numbering from patch names. If it's a git sequence named
671 # patch, the numbers might not match up since we are starting from
672 # a different revision This does assume that people are using
673 # unique shortlog values, but they ought to be anyway...
674 newpatches = [seqpatch_re.match(fname).group(2) for fname in
675 os.listdir(tempdir)]
676 for patch in existing_patches:
677 basename = seqpatch_re.match(
678 os.path.basename(patch)).group(2)
679 if basename not in newpatches:
680 removepatches.append(patch)
681 finally:
682 shutil.rmtree(tempdir)
683
684 # Get updated patches from source tree
685 tempdir = tempfile.mkdtemp(prefix='devtool')
686 try:
687 GitApplyTree.extractPatches(srctree, update_rev, tempdir)
688
689 # Match up and replace existing patches with corresponding new patches
690 updatepatches = False
691 updaterecipe = False
692 destpath = None
693 newpatches = os.listdir(tempdir)
694 if args.append:
695 patchfiles = {}
696 for patch in existing_patches:
697 patchfile = os.path.basename(patch)
698 if patchfile in newpatches:
699 patchfiles[os.path.join(tempdir, patchfile)] = patchfile
700 newpatches.remove(patchfile)
701 for patchfile in newpatches:
702 patchfiles[os.path.join(tempdir, patchfile)] = None
703
704 if patchfiles or removepatches:
705 removevalues = None
706 if removepatches:
707 srcuri = (rd.getVar('SRC_URI', False) or '').split()
708 removedentries, remaining = _remove_patch_entries(
709 srcuri, removepatches)
710 if removedentries or remaining:
711 remaining = ['file://' + os.path.basename(item) for
712 item in remaining]
713 removevalues = {'SRC_URI': removedentries + remaining}
714 _, destpath = oe.recipeutils.bbappend_recipe(
715 rd, args.append, patchfiles,
716 removevalues=removevalues)
717 else:
718 logger.info('No patches needed updating')
719 else:
720 for patch in existing_patches:
721 patchfile = os.path.basename(patch)
722 if patchfile in newpatches:
723 logger.info('Updating patch %s' % patchfile)
724 shutil.move(os.path.join(tempdir, patchfile), patch)
725 newpatches.remove(patchfile)
726 updatepatches = True
727 srcuri = (rd.getVar('SRC_URI', False) or '').split()
728 if newpatches:
729 # Add any patches left over
730 patchdir = os.path.join(os.path.dirname(recipefile),
731 rd.getVar('BPN', True))
732 bb.utils.mkdirhier(patchdir)
733 for patchfile in newpatches:
734 logger.info('Adding new patch %s' % patchfile)
735 shutil.move(os.path.join(tempdir, patchfile),
736 os.path.join(patchdir, patchfile))
737 srcuri.append('file://%s' % patchfile)
738 updaterecipe = True
739 if removepatches:
740 removedentries, _ = _remove_patch_entries(srcuri, removepatches)
741 if removedentries:
742 updaterecipe = True
743 if updaterecipe:
744 logger.info('Updating recipe %s' % os.path.basename(recipefile))
745 oe.recipeutils.patch_recipe(rd, recipefile,
746 {'SRC_URI': ' '.join(srcuri)})
747 elif not updatepatches:
748 # Neither patches nor recipe were updated
749 logger.info('No patches need updating')
750 finally:
751 shutil.rmtree(tempdir)
752
753 _remove_patch_files(args, removepatches, destpath)
754
755def _guess_recipe_update_mode(srctree, rdata):
756 """Guess the recipe update mode to use"""
757 src_uri = (rdata.getVar('SRC_URI', False) or '').split()
758 git_uris = [uri for uri in src_uri if uri.startswith('git://')]
759 if not git_uris:
760 return 'patch'
761 # Just use the first URI for now
762 uri = git_uris[0]
763 # Check remote branch
764 params = bb.fetch.decodeurl(uri)[5]
765 upstr_branch = params['branch'] if 'branch' in params else 'master'
766 # Check if current branch HEAD is found in upstream branch
767 stdout, _ = bb.process.run('git rev-parse HEAD', cwd=srctree)
768 head_rev = stdout.rstrip()
769 stdout, _ = bb.process.run('git branch -r --contains %s' % head_rev,
770 cwd=srctree)
771 remote_brs = [branch.strip() for branch in stdout.splitlines()]
772 if 'origin/' + upstr_branch in remote_brs:
773 return 'srcrev'
774
775 return 'patch'
776
777def update_recipe(args, config, basepath, workspace):
778 """Entry point for the devtool 'update-recipe' subcommand"""
779 if not args.recipename in workspace:
780 raise DevtoolError("no recipe named %s in your workspace" %
781 args.recipename)
782
783 if args.append:
784 if not os.path.exists(args.append):
785 raise DevtoolError('bbappend destination layer directory "%s" '
786 'does not exist' % args.append)
787 if not os.path.exists(os.path.join(args.append, 'conf', 'layer.conf')):
788 raise DevtoolError('conf/layer.conf not found in bbappend '
789 'destination layer "%s"' % args.append)
790
791 tinfoil = setup_tinfoil()
792
793 rd = parse_recipe(config, tinfoil, args.recipename, True)
794 if not rd:
795 return 1
796
797 srctree = workspace[args.recipename]['srctree']
798 if args.mode == 'auto':
799 mode = _guess_recipe_update_mode(srctree, rd)
800 else:
801 mode = args.mode
802
803 if mode == 'srcrev':
804 _update_recipe_srcrev(args, srctree, rd, tinfoil.config_data)
805 elif mode == 'patch':
806 _update_recipe_patch(args, config, srctree, rd, tinfoil.config_data)
807 else:
808 raise DevtoolError('update_recipe: invalid mode %s' % mode)
809
810 rf = rd.getVar('FILE', True)
811 if rf.startswith(config.workspace_path):
812 logger.warn('Recipe file %s has been updated but is inside the workspace - you will need to move it (and any associated files next to it) out to the desired layer before using "devtool reset" in order to keep any changes' % rf)
813
814 return 0
815
816
817def status(args, config, basepath, workspace):
818 """Entry point for the devtool 'status' subcommand"""
819 if workspace:
820 for recipe, value in workspace.iteritems():
821 print("%s: %s" % (recipe, value['srctree']))
822 else:
823 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')
824 return 0
825
826
827def reset(args, config, basepath, workspace):
828 """Entry point for the devtool 'reset' subcommand"""
829 import bb
830 if args.recipename:
831 if args.all:
832 raise DevtoolError("Recipe cannot be specified if -a/--all is used")
833 elif not args.recipename in workspace:
834 raise DevtoolError("no recipe named %s in your workspace" %
835 args.recipename)
836 elif not args.all:
837 raise DevtoolError("Recipe must be specified, or specify -a/--all to "
838 "reset all recipes")
839 if args.all:
840 recipes = workspace
841 else:
842 recipes = [args.recipename]
843
844 for pn in recipes:
845 if not args.no_clean:
846 logger.info('Cleaning sysroot for recipe %s...' % pn)
847 try:
848 exec_build_env_command(config.init_path, basepath, 'bitbake -c clean %s' % pn)
849 except bb.process.ExecutionError as e:
850 raise DevtoolError('Command \'%s\' failed, output:\n%s\nIf you '
851 'wish, you may specify -n/--no-clean to '
852 'skip running this command when resetting' %
853 (e.command, e.stdout))
854
855 _check_preserve(config, pn)
856
857 preservepath = os.path.join(config.workspace_path, 'attic', pn)
858 def preservedir(origdir):
859 if os.path.exists(origdir):
860 for root, dirs, files in os.walk(origdir):
861 for fn in files:
862 logger.warn('Preserving %s in %s' % (fn, preservepath))
863 bb.utils.mkdirhier(preservepath)
864 shutil.move(os.path.join(origdir, fn), os.path.join(preservepath, fn))
865 for dn in dirs:
866 os.rmdir(os.path.join(root, dn))
867 os.rmdir(origdir)
868
869 preservedir(os.path.join(config.workspace_path, 'recipes', pn))
870 # We don't automatically create this dir next to appends, but the user can
871 preservedir(os.path.join(config.workspace_path, 'appends', pn))
872
873 return 0
874
875
876def register_commands(subparsers, context):
877 """Register devtool subcommands from this plugin"""
878 parser_add = subparsers.add_parser('add', help='Add a new recipe',
879 description='Adds a new recipe')
880 parser_add.add_argument('recipename', help='Name for new recipe to add')
881 parser_add.add_argument('srctree', help='Path to external source tree')
882 parser_add.add_argument('--same-dir', '-s', help='Build in same directory as source', action="store_true")
883 parser_add.add_argument('--fetch', '-f', help='Fetch the specified URI and extract it to create the source tree', metavar='URI')
884 parser_add.add_argument('--version', '-V', help='Version to use within recipe (PV)')
885 parser_add.set_defaults(func=add)
886
887 parser_modify = subparsers.add_parser('modify', help='Modify the source for an existing recipe',
888 description='Enables modifying the source for an existing recipe',
889 formatter_class=argparse.ArgumentDefaultsHelpFormatter)
890 parser_modify.add_argument('recipename', help='Name for recipe to edit')
891 parser_modify.add_argument('srctree', help='Path to external source tree')
892 parser_modify.add_argument('--wildcard', '-w', action="store_true", help='Use wildcard for unversioned bbappend')
893 parser_modify.add_argument('--extract', '-x', action="store_true", help='Extract source as well')
894 group = parser_modify.add_mutually_exclusive_group()
895 group.add_argument('--same-dir', '-s', help='Build in same directory as source', action="store_true")
896 group.add_argument('--no-same-dir', help='Force build in a separate build directory', action="store_true")
897 parser_modify.add_argument('--branch', '-b', default="devtool", help='Name for development branch to checkout (only when using -x)')
898 parser_modify.set_defaults(func=modify)
899
900 parser_extract = subparsers.add_parser('extract', help='Extract the source for an existing recipe',
901 description='Extracts the source for an existing recipe',
902 formatter_class=argparse.ArgumentDefaultsHelpFormatter)
903 parser_extract.add_argument('recipename', help='Name for recipe to extract the source for')
904 parser_extract.add_argument('srctree', help='Path to where to extract the source tree')
905 parser_extract.add_argument('--branch', '-b', default="devtool", help='Name for development branch to checkout')
906 parser_extract.add_argument('--keep-temp', action="store_true", help='Keep temporary directory (for debugging)')
907 parser_extract.set_defaults(func=extract)
908
909 parser_update_recipe = subparsers.add_parser('update-recipe', help='Apply changes from external source tree to recipe',
910 description='Applies changes from external source tree to a recipe (updating/adding/removing patches as necessary, or by updating SRCREV)')
911 parser_update_recipe.add_argument('recipename', help='Name of recipe to update')
912 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')
913 parser_update_recipe.add_argument('--initial-rev', help='Starting revision for patches')
914 parser_update_recipe.add_argument('--append', '-a', help='Write changes to a bbappend in the specified layer instead of the recipe', metavar='LAYERDIR')
915 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')
916 parser_update_recipe.add_argument('--no-remove', '-n', action="store_true", help='Don\'t remove patches, only add or update')
917 parser_update_recipe.set_defaults(func=update_recipe)
918
919 parser_status = subparsers.add_parser('status', help='Show workspace status',
920 description='Lists recipes currently in your workspace and the paths to their respective external source trees',
921 formatter_class=argparse.ArgumentDefaultsHelpFormatter)
922 parser_status.set_defaults(func=status)
923
924 parser_reset = subparsers.add_parser('reset', help='Remove a recipe from your workspace',
925 description='Removes the specified recipe from your workspace (resetting its state)',
926 formatter_class=argparse.ArgumentDefaultsHelpFormatter)
927 parser_reset.add_argument('recipename', nargs='?', help='Recipe to reset')
928 parser_reset.add_argument('--all', '-a', action="store_true", help='Reset all recipes (clear workspace)')
929 parser_reset.add_argument('--no-clean', '-n', action="store_true", help='Don\'t clean the sysroot to remove recipe output')
930 parser_reset.set_defaults(func=reset)