blob: 6bac44bb5c1fe14e0ee957e97a3913030ef3f699 [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
36def plugin_init(pluginlist):
37 """Plugin initialization"""
38 pass
39
40def _run(cmd, cwd=''):
41 logger.debug("Running command %s> %s" % (cwd,cmd))
42 return bb.process.run('%s' % cmd, cwd=cwd)
43
44def _get_srctree(tmpdir):
45 srctree = tmpdir
46 dirs = os.listdir(tmpdir)
47 if len(dirs) == 1:
48 srctree = os.path.join(tmpdir, dirs[0])
49 return srctree
50
51def _copy_source_code(orig, dest):
52 for path in standard._ls_tree(orig):
53 dest_dir = os.path.join(dest, os.path.dirname(path))
54 bb.utils.mkdirhier(dest_dir)
55 dest_path = os.path.join(dest, path)
Patrick Williamsf1e5d692016-03-30 15:21:19 -050056 shutil.move(os.path.join(orig, path), dest_path)
Patrick Williamsc124f4f2015-09-15 14:41:29 -050057
58def _get_checksums(rf):
59 import re
60 checksums = {}
61 with open(rf) as f:
62 for line in f:
63 for cs in ['md5sum', 'sha256sum']:
64 m = re.match("^SRC_URI\[%s\].*=.*\"(.*)\"" % cs, line)
65 if m:
66 checksums[cs] = m.group(1)
67 return checksums
68
69def _replace_checksums(rf, md5, sha256):
70 if not md5 and not sha256:
71 return
72 checksums = {'md5sum':md5, 'sha256sum':sha256}
73 with open(rf + ".tmp", "w+") as tmprf:
74 with open(rf) as f:
75 for line in f:
76 m = None
77 for cs in checksums.keys():
78 m = re.match("^SRC_URI\[%s\].*=.*\"(.*)\"" % cs, line)
79 if m:
80 if checksums[cs]:
81 oldcheck = m.group(1)
82 newcheck = checksums[cs]
83 line = line.replace(oldcheck, newcheck)
84 break
85 tmprf.write(line)
86 os.rename(rf + ".tmp", rf)
87
88
89def _remove_patch_dirs(recipefolder):
90 for root, dirs, files in os.walk(recipefolder):
91 for d in dirs:
92 shutil.rmtree(os.path.join(root,d))
93
Patrick Williamsf1e5d692016-03-30 15:21:19 -050094def _recipe_contains(rd, var):
95 rf = rd.getVar('FILE', True)
96 varfiles = oe.recipeutils.get_var_files(rf, [var], rd)
97 for var, fn in varfiles.iteritems():
98 if fn and fn.startswith(os.path.dirname(rf) + os.sep):
99 return True
100 return False
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500101
102def _rename_recipe_dirs(oldpv, newpv, path):
103 for root, dirs, files in os.walk(path):
104 for olddir in dirs:
105 if olddir.find(oldpv) != -1:
106 newdir = olddir.replace(oldpv, newpv)
107 if olddir != newdir:
108 _run('mv %s %s' % (olddir, newdir))
109
110def _rename_recipe_file(bpn, oldpv, newpv, path):
111 oldrecipe = "%s_%s.bb" % (bpn, oldpv)
112 newrecipe = "%s_%s.bb" % (bpn, newpv)
113 if os.path.isfile(os.path.join(path, oldrecipe)):
114 if oldrecipe != newrecipe:
115 _run('mv %s %s' % (oldrecipe, newrecipe), cwd=path)
116 else:
117 recipe = "%s_git.bb" % bpn
118 if os.path.isfile(os.path.join(path, recipe)):
119 newrecipe = recipe
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500120 return os.path.join(path, newrecipe)
121
122def _rename_recipe_files(bpn, oldpv, newpv, path):
123 _rename_recipe_dirs(oldpv, newpv, path)
124 return _rename_recipe_file(bpn, oldpv, newpv, path)
125
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500126def _write_append(rc, srctree, same_dir, no_same_dir, rev, workspace, d):
127 """Writes an append file"""
128 if not os.path.exists(rc):
129 raise DevtoolError("bbappend not created because %s does not exist" % rc)
130
131 appendpath = os.path.join(workspace, 'appends')
132 if not os.path.exists(appendpath):
133 bb.utils.mkdirhier(appendpath)
134
135 brf = os.path.basename(os.path.splitext(rc)[0]) # rc basename
136
137 srctree = os.path.abspath(srctree)
138 pn = d.getVar('PN',True)
139 af = os.path.join(appendpath, '%s.bbappend' % brf)
140 with open(af, 'w') as f:
141 f.write('FILESEXTRAPATHS_prepend := "${THISDIR}/${PN}:"\n\n')
142 f.write('inherit externalsrc\n')
143 f.write(('# NOTE: We use pn- overrides here to avoid affecting'
144 'multiple variants in the case where the recipe uses BBCLASSEXTEND\n'))
145 f.write('EXTERNALSRC_pn-%s = "%s"\n' % (pn, srctree))
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500146 b_is_s = use_external_build(same_dir, no_same_dir, d)
147 if b_is_s:
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500148 f.write('EXTERNALSRC_BUILD_pn-%s = "%s"\n' % (pn, srctree))
149 if rev:
150 f.write('\n# initial_rev: %s\n' % rev)
151 return af
152
153def _cleanup_on_error(rf, srctree):
154 rfp = os.path.split(rf)[0] # recipe folder
155 rfpp = os.path.split(rfp)[0] # recipes folder
156 if os.path.exists(rfp):
157 shutil.rmtree(b)
158 if not len(os.listdir(rfpp)):
159 os.rmdir(rfpp)
160 srctree = os.path.abspath(srctree)
161 if os.path.exists(srctree):
162 shutil.rmtree(srctree)
163
164def _upgrade_error(e, rf, srctree):
165 if rf:
166 cleanup_on_error(rf, srctree)
167 logger.error(e)
168 raise DevtoolError(e)
169
170def _get_uri(rd):
171 srcuris = rd.getVar('SRC_URI', True).split()
172 if not len(srcuris):
173 raise DevtoolError('SRC_URI not found on recipe')
174 srcuri = srcuris[0] # it is assumed, URI is at first position
175 srcrev = '${AUTOREV}'
176 if '://' in srcuri:
177 # Fetch a URL
178 rev_re = re.compile(';rev=([^;]+)')
179 res = rev_re.search(srcuri)
180 if res:
181 srcrev = res.group(1)
182 srcuri = rev_re.sub('', srcuri)
183 return srcuri, srcrev
184
185def _extract_new_source(newpv, srctree, no_patch, srcrev, branch, keep_temp, tinfoil, rd):
186 """Extract sources of a recipe with a new version"""
187
188 def __run(cmd):
189 """Simple wrapper which calls _run with srctree as cwd"""
190 return _run(cmd, srctree)
191
192 crd = rd.createCopy()
193
194 pv = crd.getVar('PV', True)
195 crd.setVar('PV', newpv)
196
197 tmpsrctree = None
198 uri, rev = _get_uri(crd)
199 if srcrev:
200 rev = srcrev
201 if uri.startswith('git://'):
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500202 __run('git fetch')
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500203 __run('git checkout %s' % rev)
204 __run('git tag -f devtool-base-new')
205 md5 = None
206 sha256 = None
207 else:
208 __run('git checkout -b devtool-%s' % newpv)
209
210 tmpdir = tempfile.mkdtemp(prefix='devtool')
211 try:
212 md5, sha256 = scriptutils.fetch_uri(tinfoil.config_data, uri, tmpdir, rev)
213 except bb.fetch2.FetchError as e:
214 raise DevtoolError(e)
215
216 tmpsrctree = _get_srctree(tmpdir)
217
218 scrtree = os.path.abspath(srctree)
219
220 _copy_source_code(tmpsrctree, srctree)
221
222 (stdout,_) = __run('git ls-files --modified --others --exclude-standard')
223 for f in stdout.splitlines():
224 __run('git add "%s"' % f)
225
226 __run('git commit -q -m "Commit of upstream changes at version %s" --allow-empty' % newpv)
227 __run('git tag -f devtool-base-%s' % newpv)
228
229 (stdout, _) = __run('git rev-parse HEAD')
230 rev = stdout.rstrip()
231
232 if no_patch:
233 patches = oe.recipeutils.get_recipe_patches(crd)
234 if len(patches):
235 logger.warn('By user choice, the following patches will NOT be applied')
236 for patch in patches:
237 logger.warn("%s" % os.path.basename(patch))
238 else:
239 try:
240 __run('git checkout devtool-patched -b %s' % branch)
241 __run('git rebase %s' % rev)
242 if uri.startswith('git://'):
243 suffix = 'new'
244 else:
245 suffix = newpv
246 __run('git tag -f devtool-patched-%s' % suffix)
247 except bb.process.ExecutionError as e:
248 logger.warn('Command \'%s\' failed:\n%s' % (e.command, e.stdout))
249
250 if tmpsrctree:
251 if keep_temp:
252 logger.info('Preserving temporary directory %s' % tmpsrctree)
253 else:
254 shutil.rmtree(tmpsrctree)
255
256 return (rev, md5, sha256)
257
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500258def _create_new_recipe(newpv, md5, sha256, srcrev, workspace, tinfoil, rd):
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500259 """Creates the new recipe under workspace"""
260 crd = rd.createCopy()
261
262 bpn = crd.getVar('BPN', True)
263 path = os.path.join(workspace, 'recipes', bpn)
264 bb.utils.mkdirhier(path)
265 oe.recipeutils.copy_recipe_files(crd, path)
266
267 oldpv = crd.getVar('PV', True)
268 if not newpv:
269 newpv = oldpv
270 fullpath = _rename_recipe_files(bpn, oldpv, newpv, path)
271
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500272 newvalues = {}
273 if _recipe_contains(rd, 'PV') and newpv != oldpv:
274 newvalues['PV'] = newpv
275
276 if srcrev:
277 newvalues['SRCREV'] = srcrev
278
279 if newvalues:
280 rd = oe.recipeutils.parse_recipe(fullpath, None, tinfoil.config_data)
281 oe.recipeutils.patch_recipe(rd, fullpath, newvalues)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500282
283 if md5 and sha256:
284 # Unfortunately, oe.recipeutils.patch_recipe cannot update flags.
285 # once the latter feature is implemented, we should call patch_recipe
286 # instead of the following function
287 _replace_checksums(fullpath, md5, sha256)
288
289 return fullpath
290
291def upgrade(args, config, basepath, workspace):
292 """Entry point for the devtool 'upgrade' subcommand"""
293
294 if args.recipename in workspace:
295 raise DevtoolError("recipe %s is already in your workspace" % args.recipename)
296 if not args.version and not args.srcrev:
297 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")
298
299 reason = oe.recipeutils.validate_pn(args.recipename)
300 if reason:
301 raise DevtoolError(reason)
302
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500303 tinfoil = setup_tinfoil(basepath=basepath, tracking=True)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500304
305 rd = parse_recipe(config, tinfoil, args.recipename, True)
306 if not rd:
307 return 1
308
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500309 pn = rd.getVar('PN', True)
310 if pn != args.recipename:
311 logger.info('Mapping %s to %s' % (args.recipename, pn))
312 if pn in workspace:
313 raise DevtoolError("recipe %s is already in your workspace" % pn)
314
315 standard._check_compatible_recipe(pn, rd)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500316 if rd.getVar('PV', True) == args.version and rd.getVar('SRCREV', True) == args.srcrev:
317 raise DevtoolError("Current and upgrade versions are the same version" % version)
318
319 rf = None
320 try:
321 rev1 = standard._extract_source(args.srctree, False, 'devtool-orig', rd)
322 rev2, md5, sha256 = _extract_new_source(args.version, args.srctree, args.no_patch,
323 args.srcrev, args.branch, args.keep_temp,
324 tinfoil, rd)
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500325 rf = _create_new_recipe(args.version, md5, sha256, args.srcrev, config.workspace_path, tinfoil, rd)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500326 except bb.process.CmdError as e:
327 _upgrade_error(e, rf, args.srctree)
328 except DevtoolError as e:
329 _upgrade_error(e, rf, args.srctree)
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500330 standard._add_md5(config, pn, os.path.dirname(rf))
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500331
332 af = _write_append(rf, args.srctree, args.same_dir, args.no_same_dir, rev2,
333 config.workspace_path, rd)
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500334 standard._add_md5(config, pn, af)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500335 logger.info('Upgraded source extracted to %s' % args.srctree)
336 return 0
337
338def register_commands(subparsers, context):
339 """Register devtool subcommands from this plugin"""
340 parser_upgrade = subparsers.add_parser('upgrade', help='Upgrade an existing recipe',
341 description='Upgrades an existing recipe to a new upstream version')
342 parser_upgrade.add_argument('recipename', help='Name for recipe to extract the source for')
343 parser_upgrade.add_argument('srctree', help='Path to where to extract the source tree')
344 parser_upgrade.add_argument('--version', '-V', help='Version to upgrade to (PV)')
345 parser_upgrade.add_argument('--srcrev', '-S', help='Source revision to upgrade to (if fetching from an SCM such as git)')
346 parser_upgrade.add_argument('--branch', '-b', default="devtool", help='Name for new development branch to checkout (default "%(default)s")')
347 parser_upgrade.add_argument('--no-patch', action="store_true", help='Do not apply patches from the recipe to the new source code')
348 group = parser_upgrade.add_mutually_exclusive_group()
349 group.add_argument('--same-dir', '-s', help='Build in same directory as source', action="store_true")
350 group.add_argument('--no-same-dir', help='Force build in a separate build directory', action="store_true")
351 parser_upgrade.add_argument('--keep-temp', action="store_true", help='Keep temporary directory (for debugging)')
352 parser_upgrade.set_defaults(func=upgrade)