diff options
-rw-r--r-- | meta/lib/oeqa/selftest/devtool.py | 70 | ||||
-rw-r--r-- | scripts/lib/devtool/standard.py | 230 |
2 files changed, 213 insertions, 87 deletions
diff --git a/meta/lib/oeqa/selftest/devtool.py b/meta/lib/oeqa/selftest/devtool.py index 8caf07aaec..f147f248b3 100644 --- a/meta/lib/oeqa/selftest/devtool.py +++ b/meta/lib/oeqa/selftest/devtool.py | |||
@@ -233,6 +233,8 @@ class DevtoolTests(oeSelfTest): | |||
233 | self.assertTrue(not os.path.exists(workspacedir), 'This test cannot be run with a workspace directory under the build directory') | 233 | self.assertTrue(not os.path.exists(workspacedir), 'This test cannot be run with a workspace directory under the build directory') |
234 | testrecipe = 'minicom' | 234 | testrecipe = 'minicom' |
235 | recipefile = get_bb_var('FILE', testrecipe) | 235 | recipefile = get_bb_var('FILE', testrecipe) |
236 | src_uri = get_bb_var('SRC_URI', testrecipe) | ||
237 | self.assertNotIn('git://', src_uri, 'This test expects the %s recipe to NOT be a git recipe' % testrecipe) | ||
236 | result = runCmd('git status . --porcelain', cwd=os.path.dirname(recipefile)) | 238 | result = runCmd('git status . --porcelain', cwd=os.path.dirname(recipefile)) |
237 | self.assertEqual(result.output.strip(), "", '%s recipe is not clean' % testrecipe) | 239 | self.assertEqual(result.output.strip(), "", '%s recipe is not clean' % testrecipe) |
238 | # First, modify a recipe | 240 | # First, modify a recipe |
@@ -266,11 +268,77 @@ class DevtoolTests(oeSelfTest): | |||
266 | self.assertEqual(line[:3], '?? ', 'Unexpected status in line: %s' % line) | 268 | self.assertEqual(line[:3], '?? ', 'Unexpected status in line: %s' % line) |
267 | elif line.endswith('0002-Add-a-new-file.patch'): | 269 | elif line.endswith('0002-Add-a-new-file.patch'): |
268 | self.assertEqual(line[:3], '?? ', 'Unexpected status in line: %s' % line) | 270 | self.assertEqual(line[:3], '?? ', 'Unexpected status in line: %s' % line) |
269 | elif re.search('minicom_[^_]*.bb$', line): | 271 | elif re.search('%s_[^_]*.bb$' % testrecipe, line): |
270 | self.assertEqual(line[:3], ' M ', 'Unexpected status in line: %s' % line) | 272 | self.assertEqual(line[:3], ' M ', 'Unexpected status in line: %s' % line) |
271 | else: | 273 | else: |
272 | raise AssertionError('Unexpected modified file in status: %s' % line) | 274 | raise AssertionError('Unexpected modified file in status: %s' % line) |
273 | 275 | ||
276 | def test_devtool_update_recipe_git(self): | ||
277 | # Check preconditions | ||
278 | workspacedir = os.path.join(self.builddir, 'workspace') | ||
279 | self.assertTrue(not os.path.exists(workspacedir), 'This test cannot be run with a workspace directory under the build directory') | ||
280 | testrecipe = 'mtd-utils' | ||
281 | recipefile = get_bb_var('FILE', testrecipe) | ||
282 | src_uri = get_bb_var('SRC_URI', testrecipe) | ||
283 | self.assertIn('git://', src_uri, 'This test expects the %s recipe to be a git recipe' % testrecipe) | ||
284 | result = runCmd('git status . --porcelain', cwd=os.path.dirname(recipefile)) | ||
285 | self.assertEqual(result.output.strip(), "", '%s recipe is not clean' % testrecipe) | ||
286 | # First, modify a recipe | ||
287 | tempdir = tempfile.mkdtemp(prefix='devtoolqa') | ||
288 | self.track_for_cleanup(tempdir) | ||
289 | self.track_for_cleanup(workspacedir) | ||
290 | self.add_command_to_tearDown('bitbake-layers remove-layer */workspace') | ||
291 | # (don't bother with cleaning the recipe on teardown, we won't be building it) | ||
292 | result = runCmd('devtool modify %s -x %s' % (testrecipe, tempdir)) | ||
293 | # Check git repo | ||
294 | self.assertTrue(os.path.isdir(os.path.join(tempdir, '.git')), 'git repository for external source tree not found') | ||
295 | result = runCmd('git status --porcelain', cwd=tempdir) | ||
296 | self.assertEqual(result.output.strip(), "", 'Created git repo is not clean') | ||
297 | result = runCmd('git symbolic-ref HEAD', cwd=tempdir) | ||
298 | self.assertEqual(result.output.strip(), "refs/heads/devtool", 'Wrong branch in git repo') | ||
299 | # Add a couple of commits | ||
300 | # FIXME: this only tests adding, need to also test update and remove | ||
301 | result = runCmd('echo "# Additional line" >> Makefile', cwd=tempdir) | ||
302 | result = runCmd('git commit -a -m "Change the Makefile"', cwd=tempdir) | ||
303 | result = runCmd('echo "A new file" > devtool-new-file', cwd=tempdir) | ||
304 | result = runCmd('git add devtool-new-file', cwd=tempdir) | ||
305 | result = runCmd('git commit -m "Add a new file"', cwd=tempdir) | ||
306 | self.add_command_to_tearDown('cd %s; git checkout %s %s' % (os.path.dirname(recipefile), testrecipe, os.path.basename(recipefile))) | ||
307 | result = runCmd('devtool update-recipe %s' % testrecipe) | ||
308 | result = runCmd('git status . --porcelain', cwd=os.path.dirname(recipefile)) | ||
309 | self.assertNotEqual(result.output.strip(), "", '%s recipe should be modified' % testrecipe) | ||
310 | status = result.output.splitlines() | ||
311 | self.assertEqual(len(status), 3, 'Less/more files modified than expected. Entire status:\n%s' % result.output) | ||
312 | for line in status: | ||
313 | if line.endswith('add-exclusion-to-mkfs-jffs2-git-2.patch'): | ||
314 | self.assertEqual(line[:3], ' D ', 'Unexpected status in line: %s' % line) | ||
315 | elif line.endswith('fix-armv7-neon-alignment.patch'): | ||
316 | self.assertEqual(line[:3], ' D ', 'Unexpected status in line: %s' % line) | ||
317 | elif re.search('%s_[^_]*.bb$' % testrecipe, line): | ||
318 | self.assertEqual(line[:3], ' M ', 'Unexpected status in line: %s' % line) | ||
319 | else: | ||
320 | raise AssertionError('Unexpected modified file in status: %s' % line) | ||
321 | result = runCmd('git diff %s' % os.path.basename(recipefile), cwd=os.path.dirname(recipefile)) | ||
322 | addlines = ['SRCREV = ".*"', 'SRC_URI = "git://git.infradead.org/mtd-utils.git"'] | ||
323 | removelines = ['SRCREV = ".*"', 'SRC_URI = "git://git.infradead.org/mtd-utils.git \\\\', 'file://add-exclusion-to-mkfs-jffs2-git-2.patch \\\\', 'file://fix-armv7-neon-alignment.patch \\\\', '"'] | ||
324 | for line in result.output.splitlines(): | ||
325 | if line.startswith('+++') or line.startswith('---'): | ||
326 | continue | ||
327 | elif line.startswith('+'): | ||
328 | matched = False | ||
329 | for item in addlines: | ||
330 | if re.match(item, line[1:].strip()): | ||
331 | matched = True | ||
332 | break | ||
333 | self.assertTrue(matched, 'Unexpected diff add line: %s' % line) | ||
334 | elif line.startswith('-'): | ||
335 | matched = False | ||
336 | for item in removelines: | ||
337 | if re.match(item, line[1:].strip()): | ||
338 | matched = True | ||
339 | break | ||
340 | self.assertTrue(matched, 'Unexpected diff remove line: %s' % line) | ||
341 | |||
274 | def test_devtool_extract(self): | 342 | def test_devtool_extract(self): |
275 | # Check preconditions | 343 | # Check preconditions |
276 | workspacedir = os.path.join(self.builddir, 'workspace') | 344 | workspacedir = os.path.join(self.builddir, 'workspace') |
diff --git a/scripts/lib/devtool/standard.py b/scripts/lib/devtool/standard.py index 9b5a0855b2..3a8c66c131 100644 --- a/scripts/lib/devtool/standard.py +++ b/scripts/lib/devtool/standard.py | |||
@@ -359,104 +359,162 @@ def update_recipe(args, config, basepath, workspace): | |||
359 | from oe.patch import GitApplyTree | 359 | from oe.patch import GitApplyTree |
360 | import oe.recipeutils | 360 | import oe.recipeutils |
361 | 361 | ||
362 | srctree = workspace[args.recipename] | ||
363 | commits = [] | ||
364 | update_rev = None | ||
365 | if args.initial_rev: | ||
366 | initial_rev = args.initial_rev | ||
367 | else: | ||
368 | initial_rev = None | ||
369 | with open(appends[0], 'r') as f: | ||
370 | for line in f: | ||
371 | if line.startswith('# initial_rev:'): | ||
372 | initial_rev = line.split(':')[-1].strip() | ||
373 | elif line.startswith('# commit:'): | ||
374 | commits.append(line.split(':')[-1].strip()) | ||
375 | |||
376 | if initial_rev: | ||
377 | # Find first actually changed revision | ||
378 | (stdout, _) = bb.process.run('git rev-list --reverse %s..HEAD' % initial_rev, cwd=srctree) | ||
379 | newcommits = stdout.split() | ||
380 | for i in xrange(min(len(commits), len(newcommits))): | ||
381 | if newcommits[i] == commits[i]: | ||
382 | update_rev = commits[i] | ||
383 | |||
384 | if not initial_rev: | ||
385 | logger.error('Unable to find initial revision - please specify it with --initial-rev') | ||
386 | return -1 | ||
387 | |||
388 | if not update_rev: | ||
389 | update_rev = initial_rev | ||
390 | |||
391 | # Find list of existing patches in recipe file | ||
392 | recipefile = _get_recipe_file(tinfoil.cooker, args.recipename) | 362 | recipefile = _get_recipe_file(tinfoil.cooker, args.recipename) |
393 | if not recipefile: | 363 | if not recipefile: |
394 | # Error already logged | 364 | # Error already logged |
395 | return -1 | 365 | return -1 |
396 | rd = oe.recipeutils.parse_recipe(recipefile, tinfoil.config_data) | 366 | rd = oe.recipeutils.parse_recipe(recipefile, tinfoil.config_data) |
397 | existing_patches = oe.recipeutils.get_recipe_patches(rd) | ||
398 | 367 | ||
399 | removepatches = [] | 368 | orig_src_uri = rd.getVar('SRC_URI', False) or '' |
400 | if not args.no_remove: | 369 | if args.mode == 'auto': |
401 | # Get all patches from source tree and check if any should be removed | 370 | if 'git://' in orig_src_uri: |
371 | mode = 'srcrev' | ||
372 | else: | ||
373 | mode = 'patch' | ||
374 | else: | ||
375 | mode = args.mode | ||
376 | |||
377 | def remove_patches(srcuri, patchlist): | ||
378 | # Remove any patches that we don't need | ||
379 | updated = False | ||
380 | for patch in patchlist: | ||
381 | patchfile = os.path.basename(patch) | ||
382 | for i in xrange(len(srcuri)): | ||
383 | if srcuri[i].startswith('file://') and os.path.basename(srcuri[i]).split(';')[0] == patchfile: | ||
384 | logger.info('Removing patch %s' % patchfile) | ||
385 | srcuri.pop(i) | ||
386 | # FIXME "git rm" here would be nice if the file in question is tracked | ||
387 | # FIXME there's a chance that this file is referred to by another recipe, in which case deleting wouldn't be the right thing to do | ||
388 | if patch.startswith(os.path.dirname(recipefile)): | ||
389 | os.remove(patch) | ||
390 | updated = True | ||
391 | break | ||
392 | return updated | ||
393 | |||
394 | srctree = workspace[args.recipename] | ||
395 | if mode == 'srcrev': | ||
396 | (stdout, _) = bb.process.run('git rev-parse HEAD', cwd=srctree) | ||
397 | srcrev = stdout.strip() | ||
398 | if len(srcrev) != 40: | ||
399 | logger.error('Invalid hash returned by git: %s' % stdout) | ||
400 | return 1 | ||
401 | |||
402 | logger.info('Updating SRCREV in recipe %s' % os.path.basename(recipefile)) | ||
403 | patchfields = {} | ||
404 | patchfields['SRCREV'] = srcrev | ||
405 | if not args.no_remove: | ||
406 | # Find list of existing patches in recipe file | ||
407 | existing_patches = oe.recipeutils.get_recipe_patches(rd) | ||
408 | |||
409 | old_srcrev = (rd.getVar('SRCREV', False) or '') | ||
410 | tempdir = tempfile.mkdtemp(prefix='devtool') | ||
411 | removepatches = [] | ||
412 | try: | ||
413 | GitApplyTree.extractPatches(srctree, old_srcrev, tempdir) | ||
414 | newpatches = os.listdir(tempdir) | ||
415 | for patch in existing_patches: | ||
416 | patchfile = os.path.basename(patch) | ||
417 | if patchfile in newpatches: | ||
418 | removepatches.append(patch) | ||
419 | finally: | ||
420 | shutil.rmtree(tempdir) | ||
421 | if removepatches: | ||
422 | srcuri = (rd.getVar('SRC_URI', False) or '').split() | ||
423 | if remove_patches(srcuri, removepatches): | ||
424 | patchfields['SRC_URI'] = ' '.join(srcuri) | ||
425 | |||
426 | oe.recipeutils.patch_recipe(rd, recipefile, patchfields) | ||
427 | |||
428 | if not 'git://' in orig_src_uri: | ||
429 | logger.info('You will need to update SRC_URI within the recipe to point to a git repository where you have pushed your changes') | ||
430 | |||
431 | elif mode == 'patch': | ||
432 | commits = [] | ||
433 | update_rev = None | ||
434 | if args.initial_rev: | ||
435 | initial_rev = args.initial_rev | ||
436 | else: | ||
437 | initial_rev = None | ||
438 | with open(appends[0], 'r') as f: | ||
439 | for line in f: | ||
440 | if line.startswith('# initial_rev:'): | ||
441 | initial_rev = line.split(':')[-1].strip() | ||
442 | elif line.startswith('# commit:'): | ||
443 | commits.append(line.split(':')[-1].strip()) | ||
444 | |||
445 | if initial_rev: | ||
446 | # Find first actually changed revision | ||
447 | (stdout, _) = bb.process.run('git rev-list --reverse %s..HEAD' % initial_rev, cwd=srctree) | ||
448 | newcommits = stdout.split() | ||
449 | for i in xrange(min(len(commits), len(newcommits))): | ||
450 | if newcommits[i] == commits[i]: | ||
451 | update_rev = commits[i] | ||
452 | |||
453 | if not initial_rev: | ||
454 | logger.error('Unable to find initial revision - please specify it with --initial-rev') | ||
455 | return -1 | ||
456 | |||
457 | if not update_rev: | ||
458 | update_rev = initial_rev | ||
459 | |||
460 | # Find list of existing patches in recipe file | ||
461 | existing_patches = oe.recipeutils.get_recipe_patches(rd) | ||
462 | |||
463 | removepatches = [] | ||
464 | if not args.no_remove: | ||
465 | # Get all patches from source tree and check if any should be removed | ||
466 | tempdir = tempfile.mkdtemp(prefix='devtool') | ||
467 | try: | ||
468 | GitApplyTree.extractPatches(srctree, initial_rev, tempdir) | ||
469 | newpatches = os.listdir(tempdir) | ||
470 | for patch in existing_patches: | ||
471 | patchfile = os.path.basename(patch) | ||
472 | if patchfile not in newpatches: | ||
473 | removepatches.append(patch) | ||
474 | finally: | ||
475 | shutil.rmtree(tempdir) | ||
476 | |||
477 | # Get updated patches from source tree | ||
402 | tempdir = tempfile.mkdtemp(prefix='devtool') | 478 | tempdir = tempfile.mkdtemp(prefix='devtool') |
403 | try: | 479 | try: |
404 | GitApplyTree.extractPatches(srctree, initial_rev, tempdir) | 480 | GitApplyTree.extractPatches(srctree, update_rev, tempdir) |
481 | |||
482 | # Match up and replace existing patches with corresponding new patches | ||
483 | updatepatches = False | ||
484 | updaterecipe = False | ||
405 | newpatches = os.listdir(tempdir) | 485 | newpatches = os.listdir(tempdir) |
406 | for patch in existing_patches: | 486 | for patch in existing_patches: |
407 | patchfile = os.path.basename(patch) | 487 | patchfile = os.path.basename(patch) |
408 | if patchfile not in newpatches: | 488 | if patchfile in newpatches: |
409 | removepatches.append(patch) | 489 | logger.info('Updating patch %s' % patchfile) |
490 | shutil.move(os.path.join(tempdir, patchfile), patch) | ||
491 | newpatches.remove(patchfile) | ||
492 | updatepatches = True | ||
493 | srcuri = (rd.getVar('SRC_URI', False) or '').split() | ||
494 | if newpatches: | ||
495 | # Add any patches left over | ||
496 | patchdir = os.path.join(os.path.dirname(recipefile), rd.getVar('BPN', True)) | ||
497 | bb.utils.mkdirhier(patchdir) | ||
498 | for patchfile in newpatches: | ||
499 | logger.info('Adding new patch %s' % patchfile) | ||
500 | shutil.move(os.path.join(tempdir, patchfile), os.path.join(patchdir, patchfile)) | ||
501 | srcuri.append('file://%s' % patchfile) | ||
502 | updaterecipe = True | ||
503 | if removepatches: | ||
504 | if remove_patches(srcuri, removepatches): | ||
505 | updaterecipe = True | ||
506 | if updaterecipe: | ||
507 | logger.info('Updating recipe %s' % os.path.basename(recipefile)) | ||
508 | oe.recipeutils.patch_recipe(rd, recipefile, {'SRC_URI': ' '.join(srcuri)}) | ||
509 | elif not updatepatches: | ||
510 | # Neither patches nor recipe were updated | ||
511 | logger.info('No patches need updating') | ||
410 | finally: | 512 | finally: |
411 | shutil.rmtree(tempdir) | 513 | shutil.rmtree(tempdir) |
412 | 514 | ||
413 | # Get updated patches from source tree | 515 | else: |
414 | tempdir = tempfile.mkdtemp(prefix='devtool') | 516 | logger.error('update_recipe: invalid mode %s' % mode) |
415 | try: | 517 | return 1 |
416 | GitApplyTree.extractPatches(srctree, update_rev, tempdir) | ||
417 | |||
418 | # Match up and replace existing patches with corresponding new patches | ||
419 | updatepatches = False | ||
420 | updaterecipe = False | ||
421 | newpatches = os.listdir(tempdir) | ||
422 | for patch in existing_patches: | ||
423 | patchfile = os.path.basename(patch) | ||
424 | if patchfile in newpatches: | ||
425 | logger.info('Updating patch %s' % patchfile) | ||
426 | shutil.move(os.path.join(tempdir, patchfile), patch) | ||
427 | newpatches.remove(patchfile) | ||
428 | updatepatches = True | ||
429 | srcuri = (rd.getVar('SRC_URI', False) or '').split() | ||
430 | if newpatches: | ||
431 | # Add any patches left over | ||
432 | patchdir = os.path.join(os.path.dirname(recipefile), rd.getVar('BPN', True)) | ||
433 | bb.utils.mkdirhier(patchdir) | ||
434 | for patchfile in newpatches: | ||
435 | logger.info('Adding new patch %s' % patchfile) | ||
436 | shutil.move(os.path.join(tempdir, patchfile), os.path.join(patchdir, patchfile)) | ||
437 | srcuri.append('file://%s' % patchfile) | ||
438 | updaterecipe = True | ||
439 | if removepatches: | ||
440 | # Remove any patches that we don't need | ||
441 | for patch in removepatches: | ||
442 | patchfile = os.path.basename(patch) | ||
443 | for i in xrange(len(srcuri)): | ||
444 | if srcuri[i].startswith('file://') and os.path.basename(srcuri[i]).split(';')[0] == patchfile: | ||
445 | logger.info('Removing patch %s' % patchfile) | ||
446 | srcuri.pop(i) | ||
447 | # FIXME "git rm" here would be nice if the file in question is tracked | ||
448 | # FIXME there's a chance that this file is referred to by another recipe, in which case deleting wouldn't be the right thing to do | ||
449 | os.remove(patch) | ||
450 | updaterecipe = True | ||
451 | break | ||
452 | if updaterecipe: | ||
453 | logger.info('Updating recipe %s' % os.path.basename(recipefile)) | ||
454 | oe.recipeutils.patch_recipe(rd, recipefile, {'SRC_URI': ' '.join(srcuri)}) | ||
455 | elif not updatepatches: | ||
456 | # Neither patches nor recipe were updated | ||
457 | logger.info('No patches need updating') | ||
458 | finally: | ||
459 | shutil.rmtree(tempdir) | ||
460 | 518 | ||
461 | return 0 | 519 | return 0 |
462 | 520 | ||
@@ -539,9 +597,9 @@ def register_commands(subparsers, context): | |||
539 | parser_add.set_defaults(func=extract) | 597 | parser_add.set_defaults(func=extract) |
540 | 598 | ||
541 | parser_add = subparsers.add_parser('update-recipe', help='Apply changes from external source tree to recipe', | 599 | parser_add = subparsers.add_parser('update-recipe', help='Apply changes from external source tree to recipe', |
542 | description='Applies changes from external source tree to a recipe (updating/adding/removing patches as necessary)', | 600 | description='Applies changes from external source tree to a recipe (updating/adding/removing patches as necessary, or by updating SRCREV)') |
543 | formatter_class=argparse.ArgumentDefaultsHelpFormatter) | ||
544 | parser_add.add_argument('recipename', help='Name of recipe to update') | 601 | parser_add.add_argument('recipename', help='Name of recipe to update') |
602 | parser_add.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') | ||
545 | parser_add.add_argument('--initial-rev', help='Starting revision for patches') | 603 | parser_add.add_argument('--initial-rev', help='Starting revision for patches') |
546 | parser_add.add_argument('--no-remove', '-n', action="store_true", help='Don\'t remove patches, only add or update') | 604 | parser_add.add_argument('--no-remove', '-n', action="store_true", help='Don\'t remove patches, only add or update') |
547 | parser_add.set_defaults(func=update_recipe) | 605 | parser_add.set_defaults(func=update_recipe) |