blob: 0332f100f19e9d76c0883fe478ecb1dee628cd3e [file] [log] [blame]
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001import oe.path
2
3class NotFoundError(bb.BBHandledException):
4 def __init__(self, path):
5 self.path = path
6
7 def __str__(self):
8 return "Error: %s not found." % self.path
9
10class CmdError(bb.BBHandledException):
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050011 def __init__(self, command, exitstatus, output):
12 self.command = command
Patrick Williamsc124f4f2015-09-15 14:41:29 -050013 self.status = exitstatus
14 self.output = output
15
16 def __str__(self):
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050017 return "Command Error: '%s' exited with %d Output:\n%s" % \
18 (self.command, self.status, self.output)
Patrick Williamsc124f4f2015-09-15 14:41:29 -050019
20
21def runcmd(args, dir = None):
22 import pipes
23
24 if dir:
25 olddir = os.path.abspath(os.curdir)
26 if not os.path.exists(dir):
27 raise NotFoundError(dir)
28 os.chdir(dir)
29 # print("cwd: %s -> %s" % (olddir, dir))
30
31 try:
32 args = [ pipes.quote(str(arg)) for arg in args ]
33 cmd = " ".join(args)
34 # print("cmd: %s" % cmd)
35 (exitstatus, output) = oe.utils.getstatusoutput(cmd)
36 if exitstatus != 0:
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050037 raise CmdError(cmd, exitstatus >> 8, output)
Patrick Williamsc124f4f2015-09-15 14:41:29 -050038 return output
39
40 finally:
41 if dir:
42 os.chdir(olddir)
43
44class PatchError(Exception):
45 def __init__(self, msg):
46 self.msg = msg
47
48 def __str__(self):
49 return "Patch Error: %s" % self.msg
50
51class PatchSet(object):
52 defaults = {
53 "strippath": 1
54 }
55
56 def __init__(self, dir, d):
57 self.dir = dir
58 self.d = d
59 self.patches = []
60 self._current = None
61
62 def current(self):
63 return self._current
64
65 def Clean(self):
66 """
67 Clean out the patch set. Generally includes unapplying all
68 patches and wiping out all associated metadata.
69 """
70 raise NotImplementedError()
71
72 def Import(self, patch, force):
73 if not patch.get("file"):
74 if not patch.get("remote"):
75 raise PatchError("Patch file must be specified in patch import.")
76 else:
77 patch["file"] = bb.fetch2.localpath(patch["remote"], self.d)
78
79 for param in PatchSet.defaults:
80 if not patch.get(param):
81 patch[param] = PatchSet.defaults[param]
82
83 if patch.get("remote"):
84 patch["file"] = bb.data.expand(bb.fetch2.localpath(patch["remote"], self.d), self.d)
85
86 patch["filemd5"] = bb.utils.md5_file(patch["file"])
87
88 def Push(self, force):
89 raise NotImplementedError()
90
91 def Pop(self, force):
92 raise NotImplementedError()
93
94 def Refresh(self, remote = None, all = None):
95 raise NotImplementedError()
96
97 @staticmethod
98 def getPatchedFiles(patchfile, striplevel, srcdir=None):
99 """
100 Read a patch file and determine which files it will modify.
101 Params:
102 patchfile: the patch file to read
103 striplevel: the strip level at which the patch is going to be applied
104 srcdir: optional path to join onto the patched file paths
105 Returns:
106 A list of tuples of file path and change mode ('A' for add,
107 'D' for delete or 'M' for modify)
108 """
109
110 def patchedpath(patchline):
111 filepth = patchline.split()[1]
112 if filepth.endswith('/dev/null'):
113 return '/dev/null'
114 filesplit = filepth.split(os.sep)
115 if striplevel > len(filesplit):
116 bb.error('Patch %s has invalid strip level %d' % (patchfile, striplevel))
117 return None
118 return os.sep.join(filesplit[striplevel:])
119
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600120 for encoding in ['utf-8', 'latin-1']:
121 try:
122 copiedmode = False
123 filelist = []
124 with open(patchfile) as f:
125 for line in f:
126 if line.startswith('--- '):
127 patchpth = patchedpath(line)
128 if not patchpth:
129 break
130 if copiedmode:
131 addedfile = patchpth
132 else:
133 removedfile = patchpth
134 elif line.startswith('+++ '):
135 addedfile = patchedpath(line)
136 if not addedfile:
137 break
138 elif line.startswith('*** '):
139 copiedmode = True
140 removedfile = patchedpath(line)
141 if not removedfile:
142 break
143 else:
144 removedfile = None
145 addedfile = None
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500146
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600147 if addedfile and removedfile:
148 if removedfile == '/dev/null':
149 mode = 'A'
150 elif addedfile == '/dev/null':
151 mode = 'D'
152 else:
153 mode = 'M'
154 if srcdir:
155 fullpath = os.path.abspath(os.path.join(srcdir, addedfile))
156 else:
157 fullpath = addedfile
158 filelist.append((fullpath, mode))
159 except UnicodeDecodeError:
160 continue
161 break
162 else:
163 raise PatchError('Unable to decode %s' % patchfile)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500164
165 return filelist
166
167
168class PatchTree(PatchSet):
169 def __init__(self, dir, d):
170 PatchSet.__init__(self, dir, d)
171 self.patchdir = os.path.join(self.dir, 'patches')
172 self.seriespath = os.path.join(self.dir, 'patches', 'series')
173 bb.utils.mkdirhier(self.patchdir)
174
175 def _appendPatchFile(self, patch, strippath):
176 with open(self.seriespath, 'a') as f:
177 f.write(os.path.basename(patch) + "," + strippath + "\n")
178 shellcmd = ["cat", patch, ">" , self.patchdir + "/" + os.path.basename(patch)]
179 runcmd(["sh", "-c", " ".join(shellcmd)], self.dir)
180
181 def _removePatch(self, p):
182 patch = {}
183 patch['file'] = p.split(",")[0]
184 patch['strippath'] = p.split(",")[1]
185 self._applypatch(patch, False, True)
186
187 def _removePatchFile(self, all = False):
188 if not os.path.exists(self.seriespath):
189 return
190 with open(self.seriespath, 'r+') as f:
191 patches = f.readlines()
192 if all:
193 for p in reversed(patches):
194 self._removePatch(os.path.join(self.patchdir, p.strip()))
195 patches = []
196 else:
197 self._removePatch(os.path.join(self.patchdir, patches[-1].strip()))
198 patches.pop()
199 with open(self.seriespath, 'w') as f:
200 for p in patches:
201 f.write(p)
202
203 def Import(self, patch, force = None):
204 """"""
205 PatchSet.Import(self, patch, force)
206
207 if self._current is not None:
208 i = self._current + 1
209 else:
210 i = 0
211 self.patches.insert(i, patch)
212
213 def _applypatch(self, patch, force = False, reverse = False, run = True):
214 shellcmd = ["cat", patch['file'], "|", "patch", "-p", patch['strippath']]
215 if reverse:
216 shellcmd.append('-R')
217
218 if not run:
219 return "sh" + "-c" + " ".join(shellcmd)
220
221 if not force:
222 shellcmd.append('--dry-run')
223
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500224 try:
225 output = runcmd(["sh", "-c", " ".join(shellcmd)], self.dir)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500226
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500227 if force:
228 return
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500229
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500230 shellcmd.pop(len(shellcmd) - 1)
231 output = runcmd(["sh", "-c", " ".join(shellcmd)], self.dir)
232 except CmdError as err:
233 raise bb.BBHandledException("Applying '%s' failed:\n%s" %
234 (os.path.basename(patch['file']), err.output))
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500235
236 if not reverse:
237 self._appendPatchFile(patch['file'], patch['strippath'])
238
239 return output
240
241 def Push(self, force = False, all = False, run = True):
242 bb.note("self._current is %s" % self._current)
243 bb.note("patches is %s" % self.patches)
244 if all:
245 for i in self.patches:
246 bb.note("applying patch %s" % i)
247 self._applypatch(i, force)
248 self._current = i
249 else:
250 if self._current is not None:
251 next = self._current + 1
252 else:
253 next = 0
254
255 bb.note("applying patch %s" % self.patches[next])
256 ret = self._applypatch(self.patches[next], force)
257
258 self._current = next
259 return ret
260
261 def Pop(self, force = None, all = None):
262 if all:
263 self._removePatchFile(True)
264 self._current = None
265 else:
266 self._removePatchFile(False)
267
268 if self._current == 0:
269 self._current = None
270
271 if self._current is not None:
272 self._current = self._current - 1
273
274 def Clean(self):
275 """"""
276 self.Pop(all=True)
277
278class GitApplyTree(PatchTree):
279 patch_line_prefix = '%% original patch'
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500280 ignore_commit_prefix = '%% ignore'
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500281
282 def __init__(self, dir, d):
283 PatchTree.__init__(self, dir, d)
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600284 self.commituser = d.getVar('PATCH_GIT_USER_NAME', True)
285 self.commitemail = d.getVar('PATCH_GIT_USER_EMAIL', True)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500286
287 @staticmethod
288 def extractPatchHeader(patchfile):
289 """
290 Extract just the header lines from the top of a patch file
291 """
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600292 for encoding in ['utf-8', 'latin-1']:
293 lines = []
294 try:
295 with open(patchfile, 'r', encoding=encoding) as f:
296 for line in f:
297 if line.startswith('Index: ') or line.startswith('diff -') or line.startswith('---'):
298 break
299 lines.append(line)
300 except UnicodeDecodeError:
301 continue
302 break
303 else:
304 raise PatchError('Unable to find a character encoding to decode %s' % patchfile)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500305 return lines
306
307 @staticmethod
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500308 def decodeAuthor(line):
309 from email.header import decode_header
310 authorval = line.split(':', 1)[1].strip().replace('"', '')
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600311 result = decode_header(authorval)[0][0]
312 if hasattr(result, 'decode'):
313 result = result.decode('utf-8')
314 return result
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500315
316 @staticmethod
317 def interpretPatchHeader(headerlines):
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500318 import re
319 author_re = re.compile('[\S ]+ <\S+@\S+\.\S+>')
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600320 from_commit_re = re.compile('^From [a-z0-9]{40} .*')
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500321 outlines = []
322 author = None
323 date = None
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500324 subject = None
325 for line in headerlines:
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500326 if line.startswith('Subject: '):
327 subject = line.split(':', 1)[1]
328 # Remove any [PATCH][oe-core] etc.
329 subject = re.sub(r'\[.+?\]\s*', '', subject)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500330 continue
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500331 elif line.startswith('From: ') or line.startswith('Author: '):
332 authorval = GitApplyTree.decodeAuthor(line)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500333 # git is fussy about author formatting i.e. it must be Name <email@domain>
334 if author_re.match(authorval):
335 author = authorval
336 continue
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500337 elif line.startswith('Date: '):
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500338 if date is None:
339 dateval = line.split(':', 1)[1].strip()
340 # Very crude check for date format, since git will blow up if it's not in the right
341 # format. Without e.g. a python-dateutils dependency we can't do a whole lot more
342 if len(dateval) > 12:
343 date = dateval
344 continue
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500345 elif not author and line.lower().startswith('signed-off-by: '):
346 authorval = GitApplyTree.decodeAuthor(line)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500347 # git is fussy about author formatting i.e. it must be Name <email@domain>
348 if author_re.match(authorval):
349 author = authorval
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600350 elif from_commit_re.match(line):
351 # We don't want the From <commit> line - if it's present it will break rebasing
352 continue
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500353 outlines.append(line)
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600354
355 if not subject:
356 firstline = None
357 for line in headerlines:
358 line = line.strip()
359 if firstline:
360 if line:
361 # Second line is not blank, the first line probably isn't usable
362 firstline = None
363 break
364 elif line:
365 firstline = line
366 if firstline and not firstline.startswith(('#', 'Index:', 'Upstream-Status:')) and len(firstline) < 100:
367 subject = firstline
368
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500369 return outlines, author, date, subject
370
371 @staticmethod
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600372 def gitCommandUserOptions(cmd, commituser=None, commitemail=None, d=None):
373 if d:
374 commituser = d.getVar('PATCH_GIT_USER_NAME', True)
375 commitemail = d.getVar('PATCH_GIT_USER_EMAIL', True)
376 if commituser:
377 cmd += ['-c', 'user.name="%s"' % commituser]
378 if commitemail:
379 cmd += ['-c', 'user.email="%s"' % commitemail]
380
381 @staticmethod
382 def prepareCommit(patchfile, commituser=None, commitemail=None):
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500383 """
384 Prepare a git commit command line based on the header from a patch file
385 (typically this is useful for patches that cannot be applied with "git am" due to formatting)
386 """
387 import tempfile
388 # Process patch header and extract useful information
389 lines = GitApplyTree.extractPatchHeader(patchfile)
390 outlines, author, date, subject = GitApplyTree.interpretPatchHeader(lines)
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600391 if not author or not subject or not date:
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500392 try:
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600393 shellcmd = ["git", "log", "--format=email", "--follow", "--diff-filter=A", "--", patchfile]
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500394 out = runcmd(["sh", "-c", " ".join(shellcmd)], os.path.dirname(patchfile))
395 except CmdError:
396 out = None
397 if out:
398 _, newauthor, newdate, newsubject = GitApplyTree.interpretPatchHeader(out.splitlines())
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600399 if not author:
400 # If we're setting the author then the date should be set as well
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500401 author = newauthor
402 date = newdate
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600403 elif not date:
404 # If we don't do this we'll get the current date, at least this will be closer
405 date = newdate
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500406 if not subject:
407 subject = newsubject
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600408 if subject and outlines and not outlines[0].strip() == subject:
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500409 outlines.insert(0, '%s\n\n' % subject.strip())
410
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500411 # Write out commit message to a file
412 with tempfile.NamedTemporaryFile('w', delete=False) as tf:
413 tmpfile = tf.name
414 for line in outlines:
415 tf.write(line)
416 # Prepare git command
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600417 cmd = ["git"]
418 GitApplyTree.gitCommandUserOptions(cmd, commituser, commitemail)
419 cmd += ["commit", "-F", tmpfile]
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500420 # git doesn't like plain email addresses as authors
421 if author and '<' in author:
422 cmd.append('--author="%s"' % author)
423 if date:
424 cmd.append('--date="%s"' % date)
425 return (tmpfile, cmd)
426
427 @staticmethod
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500428 def extractPatches(tree, startcommit, outdir, paths=None):
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500429 import tempfile
430 import shutil
431 tempdir = tempfile.mkdtemp(prefix='oepatch')
432 try:
433 shellcmd = ["git", "format-patch", startcommit, "-o", tempdir]
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500434 if paths:
435 shellcmd.append('--')
436 shellcmd.extend(paths)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500437 out = runcmd(["sh", "-c", " ".join(shellcmd)], tree)
438 if out:
439 for srcfile in out.split():
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600440 for encoding in ['utf-8', 'latin-1']:
441 patchlines = []
442 outfile = None
443 try:
444 with open(srcfile, 'r', encoding=encoding) as f:
445 for line in f:
446 if line.startswith(GitApplyTree.patch_line_prefix):
447 outfile = line.split()[-1].strip()
448 continue
449 if line.startswith(GitApplyTree.ignore_commit_prefix):
450 continue
451 patchlines.append(line)
452 except UnicodeDecodeError:
453 continue
454 break
455 else:
456 raise PatchError('Unable to find a character encoding to decode %s' % srcfile)
457
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500458 if not outfile:
459 outfile = os.path.basename(srcfile)
460 with open(os.path.join(outdir, outfile), 'w') as of:
461 for line in patchlines:
462 of.write(line)
463 finally:
464 shutil.rmtree(tempdir)
465
466 def _applypatch(self, patch, force = False, reverse = False, run = True):
467 import shutil
468
469 def _applypatchhelper(shellcmd, patch, force = False, reverse = False, run = True):
470 if reverse:
471 shellcmd.append('-R')
472
473 shellcmd.append(patch['file'])
474
475 if not run:
476 return "sh" + "-c" + " ".join(shellcmd)
477
478 return runcmd(["sh", "-c", " ".join(shellcmd)], self.dir)
479
480 # Add hooks which add a pointer to the original patch file name in the commit message
481 reporoot = (runcmd("git rev-parse --show-toplevel".split(), self.dir) or '').strip()
482 if not reporoot:
483 raise Exception("Cannot get repository root for directory %s" % self.dir)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500484 hooks_dir = os.path.join(reporoot, '.git', 'hooks')
485 hooks_dir_backup = hooks_dir + '.devtool-orig'
486 if os.path.lexists(hooks_dir_backup):
487 raise Exception("Git hooks backup directory already exists: %s" % hooks_dir_backup)
488 if os.path.lexists(hooks_dir):
489 shutil.move(hooks_dir, hooks_dir_backup)
490 os.mkdir(hooks_dir)
491 commithook = os.path.join(hooks_dir, 'commit-msg')
492 applyhook = os.path.join(hooks_dir, 'applypatch-msg')
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500493 with open(commithook, 'w') as f:
494 # NOTE: the formatting here is significant; if you change it you'll also need to
495 # change other places which read it back
496 f.write('echo >> $1\n')
497 f.write('echo "%s: $PATCHFILE" >> $1\n' % GitApplyTree.patch_line_prefix)
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600498 os.chmod(commithook, 0o755)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500499 shutil.copy2(commithook, applyhook)
500 try:
501 patchfilevar = 'PATCHFILE="%s"' % os.path.basename(patch['file'])
502 try:
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600503 shellcmd = [patchfilevar, "git", "--work-tree=%s" % reporoot]
504 self.gitCommandUserOptions(shellcmd, self.commituser, self.commitemail)
505 shellcmd += ["am", "-3", "--keep-cr", "-p%s" % patch['strippath']]
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500506 return _applypatchhelper(shellcmd, patch, force, reverse, run)
507 except CmdError:
508 # Need to abort the git am, or we'll still be within it at the end
509 try:
510 shellcmd = ["git", "--work-tree=%s" % reporoot, "am", "--abort"]
511 runcmd(["sh", "-c", " ".join(shellcmd)], self.dir)
512 except CmdError:
513 pass
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500514 # git am won't always clean up after itself, sadly, so...
515 shellcmd = ["git", "--work-tree=%s" % reporoot, "reset", "--hard", "HEAD"]
516 runcmd(["sh", "-c", " ".join(shellcmd)], self.dir)
517 # Also need to take care of any stray untracked files
518 shellcmd = ["git", "--work-tree=%s" % reporoot, "clean", "-f"]
519 runcmd(["sh", "-c", " ".join(shellcmd)], self.dir)
520
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500521 # Fall back to git apply
522 shellcmd = ["git", "--git-dir=%s" % reporoot, "apply", "-p%s" % patch['strippath']]
523 try:
524 output = _applypatchhelper(shellcmd, patch, force, reverse, run)
525 except CmdError:
526 # Fall back to patch
527 output = PatchTree._applypatch(self, patch, force, reverse, run)
528 # Add all files
529 shellcmd = ["git", "add", "-f", "-A", "."]
530 output += runcmd(["sh", "-c", " ".join(shellcmd)], self.dir)
531 # Exclude the patches directory
532 shellcmd = ["git", "reset", "HEAD", self.patchdir]
533 output += runcmd(["sh", "-c", " ".join(shellcmd)], self.dir)
534 # Commit the result
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600535 (tmpfile, shellcmd) = self.prepareCommit(patch['file'], self.commituser, self.commitemail)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500536 try:
537 shellcmd.insert(0, patchfilevar)
538 output += runcmd(["sh", "-c", " ".join(shellcmd)], self.dir)
539 finally:
540 os.remove(tmpfile)
541 return output
542 finally:
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500543 shutil.rmtree(hooks_dir)
544 if os.path.lexists(hooks_dir_backup):
545 shutil.move(hooks_dir_backup, hooks_dir)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500546
547
548class QuiltTree(PatchSet):
549 def _runcmd(self, args, run = True):
550 quiltrc = self.d.getVar('QUILTRCFILE', True)
551 if not run:
552 return ["quilt"] + ["--quiltrc"] + [quiltrc] + args
553 runcmd(["quilt"] + ["--quiltrc"] + [quiltrc] + args, self.dir)
554
555 def _quiltpatchpath(self, file):
556 return os.path.join(self.dir, "patches", os.path.basename(file))
557
558
559 def __init__(self, dir, d):
560 PatchSet.__init__(self, dir, d)
561 self.initialized = False
562 p = os.path.join(self.dir, 'patches')
563 if not os.path.exists(p):
564 os.makedirs(p)
565
566 def Clean(self):
567 try:
568 self._runcmd(["pop", "-a", "-f"])
569 oe.path.remove(os.path.join(self.dir, "patches","series"))
570 except Exception:
571 pass
572 self.initialized = True
573
574 def InitFromDir(self):
575 # read series -> self.patches
576 seriespath = os.path.join(self.dir, 'patches', 'series')
577 if not os.path.exists(self.dir):
578 raise NotFoundError(self.dir)
579 if os.path.exists(seriespath):
580 with open(seriespath, 'r') as f:
581 for line in f.readlines():
582 patch = {}
583 parts = line.strip().split()
584 patch["quiltfile"] = self._quiltpatchpath(parts[0])
585 patch["quiltfilemd5"] = bb.utils.md5_file(patch["quiltfile"])
586 if len(parts) > 1:
587 patch["strippath"] = parts[1][2:]
588 self.patches.append(patch)
589
590 # determine which patches are applied -> self._current
591 try:
592 output = runcmd(["quilt", "applied"], self.dir)
593 except CmdError:
594 import sys
595 if sys.exc_value.output.strip() == "No patches applied":
596 return
597 else:
598 raise
599 output = [val for val in output.split('\n') if not val.startswith('#')]
600 for patch in self.patches:
601 if os.path.basename(patch["quiltfile"]) == output[-1]:
602 self._current = self.patches.index(patch)
603 self.initialized = True
604
605 def Import(self, patch, force = None):
606 if not self.initialized:
607 self.InitFromDir()
608 PatchSet.Import(self, patch, force)
609 oe.path.symlink(patch["file"], self._quiltpatchpath(patch["file"]), force=True)
610 with open(os.path.join(self.dir, "patches", "series"), "a") as f:
611 f.write(os.path.basename(patch["file"]) + " -p" + patch["strippath"] + "\n")
612 patch["quiltfile"] = self._quiltpatchpath(patch["file"])
613 patch["quiltfilemd5"] = bb.utils.md5_file(patch["quiltfile"])
614
615 # TODO: determine if the file being imported:
616 # 1) is already imported, and is the same
617 # 2) is already imported, but differs
618
619 self.patches.insert(self._current or 0, patch)
620
621
622 def Push(self, force = False, all = False, run = True):
623 # quilt push [-f]
624
625 args = ["push"]
626 if force:
627 args.append("-f")
628 if all:
629 args.append("-a")
630 if not run:
631 return self._runcmd(args, run)
632
633 self._runcmd(args)
634
635 if self._current is not None:
636 self._current = self._current + 1
637 else:
638 self._current = 0
639
640 def Pop(self, force = None, all = None):
641 # quilt pop [-f]
642 args = ["pop"]
643 if force:
644 args.append("-f")
645 if all:
646 args.append("-a")
647
648 self._runcmd(args)
649
650 if self._current == 0:
651 self._current = None
652
653 if self._current is not None:
654 self._current = self._current - 1
655
656 def Refresh(self, **kwargs):
657 if kwargs.get("remote"):
658 patch = self.patches[kwargs["patch"]]
659 if not patch:
660 raise PatchError("No patch found at index %s in patchset." % kwargs["patch"])
661 (type, host, path, user, pswd, parm) = bb.fetch.decodeurl(patch["remote"])
662 if type == "file":
663 import shutil
664 if not patch.get("file") and patch.get("remote"):
665 patch["file"] = bb.fetch2.localpath(patch["remote"], self.d)
666
667 shutil.copyfile(patch["quiltfile"], patch["file"])
668 else:
669 raise PatchError("Unable to do a remote refresh of %s, unsupported remote url scheme %s." % (os.path.basename(patch["quiltfile"]), type))
670 else:
671 # quilt refresh
672 args = ["refresh"]
673 if kwargs.get("quiltfile"):
674 args.append(os.path.basename(kwargs["quiltfile"]))
675 elif kwargs.get("patch"):
676 args.append(os.path.basename(self.patches[kwargs["patch"]]["quiltfile"]))
677 self._runcmd(args)
678
679class Resolver(object):
680 def __init__(self, patchset, terminal):
681 raise NotImplementedError()
682
683 def Resolve(self):
684 raise NotImplementedError()
685
686 def Revert(self):
687 raise NotImplementedError()
688
689 def Finalize(self):
690 raise NotImplementedError()
691
692class NOOPResolver(Resolver):
693 def __init__(self, patchset, terminal):
694 self.patchset = patchset
695 self.terminal = terminal
696
697 def Resolve(self):
698 olddir = os.path.abspath(os.curdir)
699 os.chdir(self.patchset.dir)
700 try:
701 self.patchset.Push()
702 except Exception:
703 import sys
704 os.chdir(olddir)
705 raise
706
707# Patch resolver which relies on the user doing all the work involved in the
708# resolution, with the exception of refreshing the remote copy of the patch
709# files (the urls).
710class UserResolver(Resolver):
711 def __init__(self, patchset, terminal):
712 self.patchset = patchset
713 self.terminal = terminal
714
715 # Force a push in the patchset, then drop to a shell for the user to
716 # resolve any rejected hunks
717 def Resolve(self):
718 olddir = os.path.abspath(os.curdir)
719 os.chdir(self.patchset.dir)
720 try:
721 self.patchset.Push(False)
722 except CmdError as v:
723 # Patch application failed
724 patchcmd = self.patchset.Push(True, False, False)
725
726 t = self.patchset.d.getVar('T', True)
727 if not t:
728 bb.msg.fatal("Build", "T not set")
729 bb.utils.mkdirhier(t)
730 import random
731 rcfile = "%s/bashrc.%s.%s" % (t, str(os.getpid()), random.random())
732 with open(rcfile, "w") as f:
733 f.write("echo '*** Manual patch resolution mode ***'\n")
734 f.write("echo 'Dropping to a shell, so patch rejects can be fixed manually.'\n")
735 f.write("echo 'Run \"quilt refresh\" when patch is corrected, press CTRL+D to exit.'\n")
736 f.write("echo ''\n")
737 f.write(" ".join(patchcmd) + "\n")
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600738 os.chmod(rcfile, 0o775)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500739
740 self.terminal("bash --rcfile " + rcfile, 'Patch Rejects: Please fix patch rejects manually', self.patchset.d)
741
742 # Construct a new PatchSet after the user's changes, compare the
743 # sets, checking patches for modifications, and doing a remote
744 # refresh on each.
745 oldpatchset = self.patchset
746 self.patchset = oldpatchset.__class__(self.patchset.dir, self.patchset.d)
747
748 for patch in self.patchset.patches:
749 oldpatch = None
750 for opatch in oldpatchset.patches:
751 if opatch["quiltfile"] == patch["quiltfile"]:
752 oldpatch = opatch
753
754 if oldpatch:
755 patch["remote"] = oldpatch["remote"]
756 if patch["quiltfile"] == oldpatch["quiltfile"]:
757 if patch["quiltfilemd5"] != oldpatch["quiltfilemd5"]:
758 bb.note("Patch %s has changed, updating remote url %s" % (os.path.basename(patch["quiltfile"]), patch["remote"]))
759 # user change? remote refresh
760 self.patchset.Refresh(remote=True, patch=self.patchset.patches.index(patch))
761 else:
762 # User did not fix the problem. Abort.
763 raise PatchError("Patch application failed, and user did not fix and refresh the patch.")
764 except Exception:
765 os.chdir(olddir)
766 raise
767 os.chdir(olddir)