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