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