blob: a4239f1cd26df02c6bba39582ea3e66a706ea5f6 [file] [log] [blame]
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001# Development tool - upgrade command plugin
2#
3# Copyright (C) 2014-2015 Intel Corporation
4#
5# This program is free software; you can redistribute it and/or modify
6# it under the terms of the GNU General Public License version 2 as
7# published by the Free Software Foundation.
8#
9# This program is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU General Public License for more details.
13#
14# You should have received a copy of the GNU General Public License along
15# with this program; if not, write to the Free Software Foundation, Inc.,
16# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17#
18"""Devtool upgrade plugin"""
19
20import os
21import sys
22import re
23import shutil
24import tempfile
25import logging
26import argparse
27import scriptutils
28import errno
29import bb
30import oe.recipeutils
31from devtool import standard
Patrick Williamsf1e5d692016-03-30 15:21:19 -050032from devtool import exec_build_env_command, setup_tinfoil, DevtoolError, parse_recipe, use_external_build
Patrick Williamsc124f4f2015-09-15 14:41:29 -050033
34logger = logging.getLogger('devtool')
35
Patrick Williamsc124f4f2015-09-15 14:41:29 -050036def _run(cmd, cwd=''):
37 logger.debug("Running command %s> %s" % (cwd,cmd))
38 return bb.process.run('%s' % cmd, cwd=cwd)
39
40def _get_srctree(tmpdir):
41 srctree = tmpdir
42 dirs = os.listdir(tmpdir)
43 if len(dirs) == 1:
44 srctree = os.path.join(tmpdir, dirs[0])
45 return srctree
46
47def _copy_source_code(orig, dest):
48 for path in standard._ls_tree(orig):
49 dest_dir = os.path.join(dest, os.path.dirname(path))
50 bb.utils.mkdirhier(dest_dir)
51 dest_path = os.path.join(dest, path)
Patrick Williamsf1e5d692016-03-30 15:21:19 -050052 shutil.move(os.path.join(orig, path), dest_path)
Patrick Williamsc124f4f2015-09-15 14:41:29 -050053
54def _get_checksums(rf):
55 import re
56 checksums = {}
57 with open(rf) as f:
58 for line in f:
59 for cs in ['md5sum', 'sha256sum']:
60 m = re.match("^SRC_URI\[%s\].*=.*\"(.*)\"" % cs, line)
61 if m:
62 checksums[cs] = m.group(1)
63 return checksums
64
Patrick Williamsc124f4f2015-09-15 14:41:29 -050065def _remove_patch_dirs(recipefolder):
66 for root, dirs, files in os.walk(recipefolder):
67 for d in dirs:
68 shutil.rmtree(os.path.join(root,d))
69
Patrick Williamsf1e5d692016-03-30 15:21:19 -050070def _recipe_contains(rd, var):
71 rf = rd.getVar('FILE', True)
72 varfiles = oe.recipeutils.get_var_files(rf, [var], rd)
Patrick Williamsc0f7c042017-02-23 20:41:17 -060073 for var, fn in varfiles.items():
Patrick Williamsf1e5d692016-03-30 15:21:19 -050074 if fn and fn.startswith(os.path.dirname(rf) + os.sep):
75 return True
76 return False
Patrick Williamsc124f4f2015-09-15 14:41:29 -050077
78def _rename_recipe_dirs(oldpv, newpv, path):
79 for root, dirs, files in os.walk(path):
Patrick Williamsc0f7c042017-02-23 20:41:17 -060080 # Rename directories with the version in their name
Patrick Williamsc124f4f2015-09-15 14:41:29 -050081 for olddir in dirs:
82 if olddir.find(oldpv) != -1:
83 newdir = olddir.replace(oldpv, newpv)
84 if olddir != newdir:
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050085 shutil.move(os.path.join(path, olddir), os.path.join(path, newdir))
Patrick Williamsc0f7c042017-02-23 20:41:17 -060086 # Rename any inc files with the version in their name (unusual, but possible)
87 for oldfile in files:
88 if oldfile.endswith('.inc'):
89 if oldfile.find(oldpv) != -1:
90 newfile = oldfile.replace(oldpv, newpv)
91 if oldfile != newfile:
92 os.rename(os.path.join(path, oldfile), os.path.join(path, newfile))
Patrick Williamsc124f4f2015-09-15 14:41:29 -050093
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050094def _rename_recipe_file(oldrecipe, bpn, oldpv, newpv, path):
95 oldrecipe = os.path.basename(oldrecipe)
96 if oldrecipe.endswith('_%s.bb' % oldpv):
97 newrecipe = '%s_%s.bb' % (bpn, newpv)
Patrick Williamsc124f4f2015-09-15 14:41:29 -050098 if oldrecipe != newrecipe:
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050099 shutil.move(os.path.join(path, oldrecipe), os.path.join(path, newrecipe))
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500100 else:
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500101 newrecipe = oldrecipe
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500102 return os.path.join(path, newrecipe)
103
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500104def _rename_recipe_files(oldrecipe, bpn, oldpv, newpv, path):
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500105 _rename_recipe_dirs(oldpv, newpv, path)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500106 return _rename_recipe_file(oldrecipe, bpn, oldpv, newpv, path)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500107
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600108def _write_append(rc, srctree, same_dir, no_same_dir, rev, copied, workspace, d):
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500109 """Writes an append file"""
110 if not os.path.exists(rc):
111 raise DevtoolError("bbappend not created because %s does not exist" % rc)
112
113 appendpath = os.path.join(workspace, 'appends')
114 if not os.path.exists(appendpath):
115 bb.utils.mkdirhier(appendpath)
116
117 brf = os.path.basename(os.path.splitext(rc)[0]) # rc basename
118
119 srctree = os.path.abspath(srctree)
120 pn = d.getVar('PN',True)
121 af = os.path.join(appendpath, '%s.bbappend' % brf)
122 with open(af, 'w') as f:
123 f.write('FILESEXTRAPATHS_prepend := "${THISDIR}/${PN}:"\n\n')
124 f.write('inherit externalsrc\n')
125 f.write(('# NOTE: We use pn- overrides here to avoid affecting'
126 'multiple variants in the case where the recipe uses BBCLASSEXTEND\n'))
127 f.write('EXTERNALSRC_pn-%s = "%s"\n' % (pn, srctree))
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500128 b_is_s = use_external_build(same_dir, no_same_dir, d)
129 if b_is_s:
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500130 f.write('EXTERNALSRC_BUILD_pn-%s = "%s"\n' % (pn, srctree))
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600131 f.write('\n')
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500132 if rev:
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600133 f.write('# initial_rev: %s\n' % rev)
134 if copied:
135 f.write('# original_path: %s\n' % os.path.dirname(d.getVar('FILE', True)))
136 f.write('# original_files: %s\n' % ' '.join(copied))
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500137 return af
138
139def _cleanup_on_error(rf, srctree):
140 rfp = os.path.split(rf)[0] # recipe folder
141 rfpp = os.path.split(rfp)[0] # recipes folder
142 if os.path.exists(rfp):
143 shutil.rmtree(b)
144 if not len(os.listdir(rfpp)):
145 os.rmdir(rfpp)
146 srctree = os.path.abspath(srctree)
147 if os.path.exists(srctree):
148 shutil.rmtree(srctree)
149
150def _upgrade_error(e, rf, srctree):
151 if rf:
152 cleanup_on_error(rf, srctree)
153 logger.error(e)
154 raise DevtoolError(e)
155
156def _get_uri(rd):
157 srcuris = rd.getVar('SRC_URI', True).split()
158 if not len(srcuris):
159 raise DevtoolError('SRC_URI not found on recipe')
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500160 # Get first non-local entry in SRC_URI - usually by convention it's
161 # the first entry, but not always!
162 srcuri = None
163 for entry in srcuris:
164 if not entry.startswith('file://'):
165 srcuri = entry
166 break
167 if not srcuri:
168 raise DevtoolError('Unable to find non-local entry in SRC_URI')
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500169 srcrev = '${AUTOREV}'
170 if '://' in srcuri:
171 # Fetch a URL
172 rev_re = re.compile(';rev=([^;]+)')
173 res = rev_re.search(srcuri)
174 if res:
175 srcrev = res.group(1)
176 srcuri = rev_re.sub('', srcuri)
177 return srcuri, srcrev
178
179def _extract_new_source(newpv, srctree, no_patch, srcrev, branch, keep_temp, tinfoil, rd):
180 """Extract sources of a recipe with a new version"""
181
182 def __run(cmd):
183 """Simple wrapper which calls _run with srctree as cwd"""
184 return _run(cmd, srctree)
185
186 crd = rd.createCopy()
187
188 pv = crd.getVar('PV', True)
189 crd.setVar('PV', newpv)
190
191 tmpsrctree = None
192 uri, rev = _get_uri(crd)
193 if srcrev:
194 rev = srcrev
195 if uri.startswith('git://'):
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500196 __run('git fetch')
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500197 __run('git checkout %s' % rev)
198 __run('git tag -f devtool-base-new')
199 md5 = None
200 sha256 = None
201 else:
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500202 __run('git checkout devtool-base -b devtool-%s' % newpv)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500203
204 tmpdir = tempfile.mkdtemp(prefix='devtool')
205 try:
206 md5, sha256 = scriptutils.fetch_uri(tinfoil.config_data, uri, tmpdir, rev)
207 except bb.fetch2.FetchError as e:
208 raise DevtoolError(e)
209
210 tmpsrctree = _get_srctree(tmpdir)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500211 srctree = os.path.abspath(srctree)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500212
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500213 # Delete all sources so we ensure no stray files are left over
214 for item in os.listdir(srctree):
215 if item in ['.git', 'oe-local-files']:
216 continue
217 itempath = os.path.join(srctree, item)
218 if os.path.isdir(itempath):
219 shutil.rmtree(itempath)
220 else:
221 os.remove(itempath)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500222
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500223 # Copy in new ones
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500224 _copy_source_code(tmpsrctree, srctree)
225
226 (stdout,_) = __run('git ls-files --modified --others --exclude-standard')
227 for f in stdout.splitlines():
228 __run('git add "%s"' % f)
229
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600230 useroptions = []
231 oe.patch.GitApplyTree.gitCommandUserOptions(useroptions, d=rd)
232 __run('git %s commit -q -m "Commit of upstream changes at version %s" --allow-empty' % (' '.join(useroptions), newpv))
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500233 __run('git tag -f devtool-base-%s' % newpv)
234
235 (stdout, _) = __run('git rev-parse HEAD')
236 rev = stdout.rstrip()
237
238 if no_patch:
239 patches = oe.recipeutils.get_recipe_patches(crd)
240 if len(patches):
241 logger.warn('By user choice, the following patches will NOT be applied')
242 for patch in patches:
243 logger.warn("%s" % os.path.basename(patch))
244 else:
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600245 __run('git checkout devtool-patched -b %s' % branch)
246 skiptag = False
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500247 try:
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500248 __run('git rebase %s' % rev)
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600249 except bb.process.ExecutionError as e:
250 skiptag = True
251 if 'conflict' in e.stdout:
252 logger.warn('Command \'%s\' failed:\n%s\n\nYou will need to resolve conflicts in order to complete the upgrade.' % (e.command, e.stdout.rstrip()))
253 else:
254 logger.warn('Command \'%s\' failed:\n%s' % (e.command, e.stdout))
255 if not skiptag:
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500256 if uri.startswith('git://'):
257 suffix = 'new'
258 else:
259 suffix = newpv
260 __run('git tag -f devtool-patched-%s' % suffix)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500261
262 if tmpsrctree:
263 if keep_temp:
264 logger.info('Preserving temporary directory %s' % tmpsrctree)
265 else:
266 shutil.rmtree(tmpsrctree)
267
268 return (rev, md5, sha256)
269
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500270def _create_new_recipe(newpv, md5, sha256, srcrev, srcbranch, workspace, tinfoil, rd):
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500271 """Creates the new recipe under workspace"""
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500272
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500273 bpn = rd.getVar('BPN', True)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500274 path = os.path.join(workspace, 'recipes', bpn)
275 bb.utils.mkdirhier(path)
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600276 copied, _ = oe.recipeutils.copy_recipe_files(rd, path)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500277
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500278 oldpv = rd.getVar('PV', True)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500279 if not newpv:
280 newpv = oldpv
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500281 origpath = rd.getVar('FILE', True)
282 fullpath = _rename_recipe_files(origpath, bpn, oldpv, newpv, path)
283 logger.debug('Upgraded %s => %s' % (origpath, fullpath))
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500284
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500285 newvalues = {}
286 if _recipe_contains(rd, 'PV') and newpv != oldpv:
287 newvalues['PV'] = newpv
288
289 if srcrev:
290 newvalues['SRCREV'] = srcrev
291
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500292 if srcbranch:
293 src_uri = oe.recipeutils.split_var_value(rd.getVar('SRC_URI', False) or '')
294 changed = False
295 replacing = True
296 new_src_uri = []
297 for entry in src_uri:
298 scheme, network, path, user, passwd, params = bb.fetch2.decodeurl(entry)
299 if replacing and scheme in ['git', 'gitsm']:
300 branch = params.get('branch', 'master')
301 if rd.expand(branch) != srcbranch:
302 # Handle case where branch is set through a variable
303 res = re.match(r'\$\{([^}@]+)\}', branch)
304 if res:
305 newvalues[res.group(1)] = srcbranch
306 # We know we won't change SRC_URI now, so break out
307 break
308 else:
309 params['branch'] = srcbranch
310 entry = bb.fetch2.encodeurl((scheme, network, path, user, passwd, params))
311 changed = True
312 replacing = False
313 new_src_uri.append(entry)
314 if changed:
315 newvalues['SRC_URI'] = ' '.join(new_src_uri)
316
317 newvalues['PR'] = None
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500318
319 if md5 and sha256:
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500320 newvalues['SRC_URI[md5sum]'] = md5
321 newvalues['SRC_URI[sha256sum]'] = sha256
322
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600323 rd = oe.recipeutils.parse_recipe(tinfoil.cooker, fullpath, None)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500324 oe.recipeutils.patch_recipe(rd, fullpath, newvalues)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500325
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600326 return fullpath, copied
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500327
328def upgrade(args, config, basepath, workspace):
329 """Entry point for the devtool 'upgrade' subcommand"""
330
331 if args.recipename in workspace:
332 raise DevtoolError("recipe %s is already in your workspace" % args.recipename)
333 if not args.version and not args.srcrev:
334 raise DevtoolError("You must provide a version using the --version/-V option, or for recipes that fetch from an SCM such as git, the --srcrev/-S option")
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500335 if args.srcbranch and not args.srcrev:
336 raise DevtoolError("If you specify --srcbranch/-B then you must use --srcrev/-S to specify the revision" % args.recipename)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500337
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500338 tinfoil = setup_tinfoil(basepath=basepath, tracking=True)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500339 try:
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600340 rd = parse_recipe(config, tinfoil, args.recipename, True)
341 if not rd:
342 return 1
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500343
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600344 pn = rd.getVar('PN', True)
345 if pn != args.recipename:
346 logger.info('Mapping %s to %s' % (args.recipename, pn))
347 if pn in workspace:
348 raise DevtoolError("recipe %s is already in your workspace" % pn)
349
350 if args.srctree:
351 srctree = os.path.abspath(args.srctree)
352 else:
353 srctree = standard.get_default_srctree(config, pn)
354
355 standard._check_compatible_recipe(pn, rd)
356 old_srcrev = rd.getVar('SRCREV', True)
357 if old_srcrev == 'INVALID':
358 old_srcrev = None
359 if old_srcrev and not args.srcrev:
360 raise DevtoolError("Recipe specifies a SRCREV value; you must specify a new one when upgrading")
361 if rd.getVar('PV', True) == args.version and old_srcrev == args.srcrev:
362 raise DevtoolError("Current and upgrade versions are the same version")
363
364 rf = None
365 try:
366 rev1 = standard._extract_source(srctree, False, 'devtool-orig', False, rd)
367 rev2, md5, sha256 = _extract_new_source(args.version, srctree, args.no_patch,
368 args.srcrev, args.branch, args.keep_temp,
369 tinfoil, rd)
370 rf, copied = _create_new_recipe(args.version, md5, sha256, args.srcrev, args.srcbranch, config.workspace_path, tinfoil, rd)
371 except bb.process.CmdError as e:
372 _upgrade_error(e, rf, srctree)
373 except DevtoolError as e:
374 _upgrade_error(e, rf, srctree)
375 standard._add_md5(config, pn, os.path.dirname(rf))
376
377 af = _write_append(rf, srctree, args.same_dir, args.no_same_dir, rev2,
378 copied, config.workspace_path, rd)
379 standard._add_md5(config, pn, af)
380 logger.info('Upgraded source extracted to %s' % srctree)
381 logger.info('New recipe is %s' % rf)
382 finally:
383 tinfoil.shutdown()
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500384 return 0
385
386def register_commands(subparsers, context):
387 """Register devtool subcommands from this plugin"""
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500388
389 defsrctree = standard.get_default_srctree(context.config)
390
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500391 parser_upgrade = subparsers.add_parser('upgrade', help='Upgrade an existing recipe',
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500392 description='Upgrades an existing recipe to a new upstream version. Puts the upgraded recipe file into the workspace along with any associated files, and extracts the source tree to a specified location (in case patches need rebasing or adding to as a result of the upgrade).',
393 group='starting')
394 parser_upgrade.add_argument('recipename', help='Name of recipe to upgrade (just name - no version, path or extension)')
395 parser_upgrade.add_argument('srctree', nargs='?', help='Path to where to extract the source tree. If not specified, a subdirectory of %s will be used.' % defsrctree)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500396 parser_upgrade.add_argument('--version', '-V', help='Version to upgrade to (PV)')
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600397 parser_upgrade.add_argument('--srcrev', '-S', help='Source revision to upgrade to (required if fetching from an SCM such as git)')
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500398 parser_upgrade.add_argument('--srcbranch', '-B', help='Branch in source repository containing the revision to use (if fetching from an SCM such as git)')
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500399 parser_upgrade.add_argument('--branch', '-b', default="devtool", help='Name for new development branch to checkout (default "%(default)s")')
400 parser_upgrade.add_argument('--no-patch', action="store_true", help='Do not apply patches from the recipe to the new source code')
401 group = parser_upgrade.add_mutually_exclusive_group()
402 group.add_argument('--same-dir', '-s', help='Build in same directory as source', action="store_true")
403 group.add_argument('--no-same-dir', help='Force build in a separate build directory', action="store_true")
404 parser_upgrade.add_argument('--keep-temp', action="store_true", help='Keep temporary directory (for debugging)')
405 parser_upgrade.set_defaults(func=upgrade)