blob: 584bf6c05fb209f084bc436c6d3e5f15c04d32f4 [file] [log] [blame]
Patrick Williamsc124f4f2015-09-15 14:41:29 -05001import oe.path
Brad Bishopd7bf8c12018-02-25 22:55:05 -05002import oe.types
Patrick Williamsc124f4f2015-09-15 14:41:29 -05003
4class NotFoundError(bb.BBHandledException):
5 def __init__(self, path):
6 self.path = path
7
8 def __str__(self):
9 return "Error: %s not found." % self.path
10
11class CmdError(bb.BBHandledException):
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050012 def __init__(self, command, exitstatus, output):
13 self.command = command
Patrick Williamsc124f4f2015-09-15 14:41:29 -050014 self.status = exitstatus
15 self.output = output
16
17 def __str__(self):
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050018 return "Command Error: '%s' exited with %d Output:\n%s" % \
19 (self.command, self.status, self.output)
Patrick Williamsc124f4f2015-09-15 14:41:29 -050020
21
22def runcmd(args, dir = None):
23 import pipes
24
25 if dir:
26 olddir = os.path.abspath(os.curdir)
27 if not os.path.exists(dir):
28 raise NotFoundError(dir)
29 os.chdir(dir)
30 # print("cwd: %s -> %s" % (olddir, dir))
31
32 try:
33 args = [ pipes.quote(str(arg)) for arg in args ]
34 cmd = " ".join(args)
35 # print("cmd: %s" % cmd)
36 (exitstatus, output) = oe.utils.getstatusoutput(cmd)
37 if exitstatus != 0:
Patrick Williamsd8c66bc2016-06-20 12:57:21 -050038 raise CmdError(cmd, exitstatus >> 8, output)
Patrick Williamsc124f4f2015-09-15 14:41:29 -050039 return output
40
41 finally:
42 if dir:
43 os.chdir(olddir)
44
45class PatchError(Exception):
46 def __init__(self, msg):
47 self.msg = msg
48
49 def __str__(self):
50 return "Patch Error: %s" % self.msg
51
52class PatchSet(object):
53 defaults = {
54 "strippath": 1
55 }
56
57 def __init__(self, dir, d):
58 self.dir = dir
59 self.d = d
60 self.patches = []
61 self._current = None
62
63 def current(self):
64 return self._current
65
66 def Clean(self):
67 """
68 Clean out the patch set. Generally includes unapplying all
69 patches and wiping out all associated metadata.
70 """
71 raise NotImplementedError()
72
73 def Import(self, patch, force):
74 if not patch.get("file"):
75 if not patch.get("remote"):
76 raise PatchError("Patch file must be specified in patch import.")
77 else:
78 patch["file"] = bb.fetch2.localpath(patch["remote"], self.d)
79
80 for param in PatchSet.defaults:
81 if not patch.get(param):
82 patch[param] = PatchSet.defaults[param]
83
84 if patch.get("remote"):
Brad Bishop6e60e8b2018-02-01 10:27:11 -050085 patch["file"] = self.d.expand(bb.fetch2.localpath(patch["remote"], self.d))
Patrick Williamsc124f4f2015-09-15 14:41:29 -050086
87 patch["filemd5"] = bb.utils.md5_file(patch["file"])
88
89 def Push(self, force):
90 raise NotImplementedError()
91
92 def Pop(self, force):
93 raise NotImplementedError()
94
95 def Refresh(self, remote = None, all = None):
96 raise NotImplementedError()
97
98 @staticmethod
99 def getPatchedFiles(patchfile, striplevel, srcdir=None):
100 """
101 Read a patch file and determine which files it will modify.
102 Params:
103 patchfile: the patch file to read
104 striplevel: the strip level at which the patch is going to be applied
105 srcdir: optional path to join onto the patched file paths
106 Returns:
107 A list of tuples of file path and change mode ('A' for add,
108 'D' for delete or 'M' for modify)
109 """
110
111 def patchedpath(patchline):
112 filepth = patchline.split()[1]
113 if filepth.endswith('/dev/null'):
114 return '/dev/null'
115 filesplit = filepth.split(os.sep)
116 if striplevel > len(filesplit):
117 bb.error('Patch %s has invalid strip level %d' % (patchfile, striplevel))
118 return None
119 return os.sep.join(filesplit[striplevel:])
120
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600121 for encoding in ['utf-8', 'latin-1']:
122 try:
123 copiedmode = False
124 filelist = []
125 with open(patchfile) as f:
126 for line in f:
127 if line.startswith('--- '):
128 patchpth = patchedpath(line)
129 if not patchpth:
130 break
131 if copiedmode:
132 addedfile = patchpth
133 else:
134 removedfile = patchpth
135 elif line.startswith('+++ '):
136 addedfile = patchedpath(line)
137 if not addedfile:
138 break
139 elif line.startswith('*** '):
140 copiedmode = True
141 removedfile = patchedpath(line)
142 if not removedfile:
143 break
144 else:
145 removedfile = None
146 addedfile = None
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500147
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600148 if addedfile and removedfile:
149 if removedfile == '/dev/null':
150 mode = 'A'
151 elif addedfile == '/dev/null':
152 mode = 'D'
153 else:
154 mode = 'M'
155 if srcdir:
156 fullpath = os.path.abspath(os.path.join(srcdir, addedfile))
157 else:
158 fullpath = addedfile
159 filelist.append((fullpath, mode))
160 except UnicodeDecodeError:
161 continue
162 break
163 else:
164 raise PatchError('Unable to decode %s' % patchfile)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500165
166 return filelist
167
168
169class PatchTree(PatchSet):
170 def __init__(self, dir, d):
171 PatchSet.__init__(self, dir, d)
172 self.patchdir = os.path.join(self.dir, 'patches')
173 self.seriespath = os.path.join(self.dir, 'patches', 'series')
174 bb.utils.mkdirhier(self.patchdir)
175
176 def _appendPatchFile(self, patch, strippath):
177 with open(self.seriespath, 'a') as f:
178 f.write(os.path.basename(patch) + "," + strippath + "\n")
179 shellcmd = ["cat", patch, ">" , self.patchdir + "/" + os.path.basename(patch)]
180 runcmd(["sh", "-c", " ".join(shellcmd)], self.dir)
181
182 def _removePatch(self, p):
183 patch = {}
184 patch['file'] = p.split(",")[0]
185 patch['strippath'] = p.split(",")[1]
186 self._applypatch(patch, False, True)
187
188 def _removePatchFile(self, all = False):
189 if not os.path.exists(self.seriespath):
190 return
191 with open(self.seriespath, 'r+') as f:
192 patches = f.readlines()
193 if all:
194 for p in reversed(patches):
195 self._removePatch(os.path.join(self.patchdir, p.strip()))
196 patches = []
197 else:
198 self._removePatch(os.path.join(self.patchdir, patches[-1].strip()))
199 patches.pop()
200 with open(self.seriespath, 'w') as f:
201 for p in patches:
202 f.write(p)
203
204 def Import(self, patch, force = None):
205 """"""
206 PatchSet.Import(self, patch, force)
207
208 if self._current is not None:
209 i = self._current + 1
210 else:
211 i = 0
212 self.patches.insert(i, patch)
213
214 def _applypatch(self, patch, force = False, reverse = False, run = True):
215 shellcmd = ["cat", patch['file'], "|", "patch", "-p", patch['strippath']]
216 if reverse:
217 shellcmd.append('-R')
218
219 if not run:
220 return "sh" + "-c" + " ".join(shellcmd)
221
222 if not force:
223 shellcmd.append('--dry-run')
224
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500225 try:
226 output = runcmd(["sh", "-c", " ".join(shellcmd)], self.dir)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500227
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500228 if force:
229 return
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500230
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500231 shellcmd.pop(len(shellcmd) - 1)
232 output = runcmd(["sh", "-c", " ".join(shellcmd)], self.dir)
233 except CmdError as err:
234 raise bb.BBHandledException("Applying '%s' failed:\n%s" %
235 (os.path.basename(patch['file']), err.output))
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500236
237 if not reverse:
238 self._appendPatchFile(patch['file'], patch['strippath'])
239
240 return output
241
242 def Push(self, force = False, all = False, run = True):
243 bb.note("self._current is %s" % self._current)
244 bb.note("patches is %s" % self.patches)
245 if all:
246 for i in self.patches:
247 bb.note("applying patch %s" % i)
248 self._applypatch(i, force)
249 self._current = i
250 else:
251 if self._current is not None:
252 next = self._current + 1
253 else:
254 next = 0
255
256 bb.note("applying patch %s" % self.patches[next])
257 ret = self._applypatch(self.patches[next], force)
258
259 self._current = next
260 return ret
261
262 def Pop(self, force = None, all = None):
263 if all:
264 self._removePatchFile(True)
265 self._current = None
266 else:
267 self._removePatchFile(False)
268
269 if self._current == 0:
270 self._current = None
271
272 if self._current is not None:
273 self._current = self._current - 1
274
275 def Clean(self):
276 """"""
277 self.Pop(all=True)
278
279class GitApplyTree(PatchTree):
280 patch_line_prefix = '%% original patch'
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500281 ignore_commit_prefix = '%% ignore'
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500282
283 def __init__(self, dir, d):
284 PatchTree.__init__(self, dir, d)
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500285 self.commituser = d.getVar('PATCH_GIT_USER_NAME')
286 self.commitemail = d.getVar('PATCH_GIT_USER_EMAIL')
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500287
288 @staticmethod
289 def extractPatchHeader(patchfile):
290 """
291 Extract just the header lines from the top of a patch file
292 """
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600293 for encoding in ['utf-8', 'latin-1']:
294 lines = []
295 try:
296 with open(patchfile, 'r', encoding=encoding) as f:
297 for line in f:
298 if line.startswith('Index: ') or line.startswith('diff -') or line.startswith('---'):
299 break
300 lines.append(line)
301 except UnicodeDecodeError:
302 continue
303 break
304 else:
305 raise PatchError('Unable to find a character encoding to decode %s' % patchfile)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500306 return lines
307
308 @staticmethod
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500309 def decodeAuthor(line):
310 from email.header import decode_header
311 authorval = line.split(':', 1)[1].strip().replace('"', '')
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600312 result = decode_header(authorval)[0][0]
313 if hasattr(result, 'decode'):
314 result = result.decode('utf-8')
315 return result
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500316
317 @staticmethod
318 def interpretPatchHeader(headerlines):
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500319 import re
320 author_re = re.compile('[\S ]+ <\S+@\S+\.\S+>')
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600321 from_commit_re = re.compile('^From [a-z0-9]{40} .*')
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500322 outlines = []
323 author = None
324 date = None
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500325 subject = None
326 for line in headerlines:
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500327 if line.startswith('Subject: '):
328 subject = line.split(':', 1)[1]
329 # Remove any [PATCH][oe-core] etc.
330 subject = re.sub(r'\[.+?\]\s*', '', subject)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500331 continue
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500332 elif line.startswith('From: ') or line.startswith('Author: '):
333 authorval = GitApplyTree.decodeAuthor(line)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500334 # git is fussy about author formatting i.e. it must be Name <email@domain>
335 if author_re.match(authorval):
336 author = authorval
337 continue
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500338 elif line.startswith('Date: '):
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500339 if date is None:
340 dateval = line.split(':', 1)[1].strip()
341 # Very crude check for date format, since git will blow up if it's not in the right
342 # format. Without e.g. a python-dateutils dependency we can't do a whole lot more
343 if len(dateval) > 12:
344 date = dateval
345 continue
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500346 elif not author and line.lower().startswith('signed-off-by: '):
347 authorval = GitApplyTree.decodeAuthor(line)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500348 # git is fussy about author formatting i.e. it must be Name <email@domain>
349 if author_re.match(authorval):
350 author = authorval
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600351 elif from_commit_re.match(line):
352 # We don't want the From <commit> line - if it's present it will break rebasing
353 continue
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500354 outlines.append(line)
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600355
356 if not subject:
357 firstline = None
358 for line in headerlines:
359 line = line.strip()
360 if firstline:
361 if line:
362 # Second line is not blank, the first line probably isn't usable
363 firstline = None
364 break
365 elif line:
366 firstline = line
367 if firstline and not firstline.startswith(('#', 'Index:', 'Upstream-Status:')) and len(firstline) < 100:
368 subject = firstline
369
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500370 return outlines, author, date, subject
371
372 @staticmethod
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600373 def gitCommandUserOptions(cmd, commituser=None, commitemail=None, d=None):
374 if d:
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500375 commituser = d.getVar('PATCH_GIT_USER_NAME')
376 commitemail = d.getVar('PATCH_GIT_USER_EMAIL')
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600377 if commituser:
378 cmd += ['-c', 'user.name="%s"' % commituser]
379 if commitemail:
380 cmd += ['-c', 'user.email="%s"' % commitemail]
381
382 @staticmethod
383 def prepareCommit(patchfile, commituser=None, commitemail=None):
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500384 """
385 Prepare a git commit command line based on the header from a patch file
386 (typically this is useful for patches that cannot be applied with "git am" due to formatting)
387 """
388 import tempfile
389 # Process patch header and extract useful information
390 lines = GitApplyTree.extractPatchHeader(patchfile)
391 outlines, author, date, subject = GitApplyTree.interpretPatchHeader(lines)
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600392 if not author or not subject or not date:
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500393 try:
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600394 shellcmd = ["git", "log", "--format=email", "--follow", "--diff-filter=A", "--", patchfile]
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500395 out = runcmd(["sh", "-c", " ".join(shellcmd)], os.path.dirname(patchfile))
396 except CmdError:
397 out = None
398 if out:
399 _, newauthor, newdate, newsubject = GitApplyTree.interpretPatchHeader(out.splitlines())
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600400 if not author:
401 # If we're setting the author then the date should be set as well
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500402 author = newauthor
403 date = newdate
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600404 elif not date:
405 # If we don't do this we'll get the current date, at least this will be closer
406 date = newdate
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500407 if not subject:
408 subject = newsubject
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600409 if subject and outlines and not outlines[0].strip() == subject:
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500410 outlines.insert(0, '%s\n\n' % subject.strip())
411
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500412 # Write out commit message to a file
413 with tempfile.NamedTemporaryFile('w', delete=False) as tf:
414 tmpfile = tf.name
415 for line in outlines:
416 tf.write(line)
417 # Prepare git command
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600418 cmd = ["git"]
419 GitApplyTree.gitCommandUserOptions(cmd, commituser, commitemail)
420 cmd += ["commit", "-F", tmpfile]
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500421 # git doesn't like plain email addresses as authors
422 if author and '<' in author:
423 cmd.append('--author="%s"' % author)
424 if date:
425 cmd.append('--date="%s"' % date)
426 return (tmpfile, cmd)
427
428 @staticmethod
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500429 def extractPatches(tree, startcommit, outdir, paths=None):
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500430 import tempfile
431 import shutil
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500432 import re
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500433 tempdir = tempfile.mkdtemp(prefix='oepatch')
434 try:
435 shellcmd = ["git", "format-patch", startcommit, "-o", tempdir]
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500436 if paths:
437 shellcmd.append('--')
438 shellcmd.extend(paths)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500439 out = runcmd(["sh", "-c", " ".join(shellcmd)], tree)
440 if out:
441 for srcfile in out.split():
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600442 for encoding in ['utf-8', 'latin-1']:
443 patchlines = []
444 outfile = None
445 try:
446 with open(srcfile, 'r', encoding=encoding) as f:
447 for line in f:
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500448 checkline = line
449 if checkline.startswith('Subject: '):
450 checkline = re.sub(r'\[.+?\]\s*', '', checkline[9:])
451 if checkline.startswith(GitApplyTree.patch_line_prefix):
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600452 outfile = line.split()[-1].strip()
453 continue
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500454 if checkline.startswith(GitApplyTree.ignore_commit_prefix):
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600455 continue
456 patchlines.append(line)
457 except UnicodeDecodeError:
458 continue
459 break
460 else:
461 raise PatchError('Unable to find a character encoding to decode %s' % srcfile)
462
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500463 if not outfile:
464 outfile = os.path.basename(srcfile)
465 with open(os.path.join(outdir, outfile), 'w') as of:
466 for line in patchlines:
467 of.write(line)
468 finally:
469 shutil.rmtree(tempdir)
470
471 def _applypatch(self, patch, force = False, reverse = False, run = True):
472 import shutil
473
474 def _applypatchhelper(shellcmd, patch, force = False, reverse = False, run = True):
475 if reverse:
476 shellcmd.append('-R')
477
478 shellcmd.append(patch['file'])
479
480 if not run:
481 return "sh" + "-c" + " ".join(shellcmd)
482
483 return runcmd(["sh", "-c", " ".join(shellcmd)], self.dir)
484
485 # Add hooks which add a pointer to the original patch file name in the commit message
486 reporoot = (runcmd("git rev-parse --show-toplevel".split(), self.dir) or '').strip()
487 if not reporoot:
488 raise Exception("Cannot get repository root for directory %s" % self.dir)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500489 hooks_dir = os.path.join(reporoot, '.git', 'hooks')
490 hooks_dir_backup = hooks_dir + '.devtool-orig'
491 if os.path.lexists(hooks_dir_backup):
492 raise Exception("Git hooks backup directory already exists: %s" % hooks_dir_backup)
493 if os.path.lexists(hooks_dir):
494 shutil.move(hooks_dir, hooks_dir_backup)
495 os.mkdir(hooks_dir)
496 commithook = os.path.join(hooks_dir, 'commit-msg')
497 applyhook = os.path.join(hooks_dir, 'applypatch-msg')
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500498 with open(commithook, 'w') as f:
499 # NOTE: the formatting here is significant; if you change it you'll also need to
500 # change other places which read it back
501 f.write('echo >> $1\n')
502 f.write('echo "%s: $PATCHFILE" >> $1\n' % GitApplyTree.patch_line_prefix)
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600503 os.chmod(commithook, 0o755)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500504 shutil.copy2(commithook, applyhook)
505 try:
506 patchfilevar = 'PATCHFILE="%s"' % os.path.basename(patch['file'])
507 try:
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600508 shellcmd = [patchfilevar, "git", "--work-tree=%s" % reporoot]
509 self.gitCommandUserOptions(shellcmd, self.commituser, self.commitemail)
510 shellcmd += ["am", "-3", "--keep-cr", "-p%s" % patch['strippath']]
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500511 return _applypatchhelper(shellcmd, patch, force, reverse, run)
512 except CmdError:
513 # Need to abort the git am, or we'll still be within it at the end
514 try:
515 shellcmd = ["git", "--work-tree=%s" % reporoot, "am", "--abort"]
516 runcmd(["sh", "-c", " ".join(shellcmd)], self.dir)
517 except CmdError:
518 pass
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500519 # git am won't always clean up after itself, sadly, so...
520 shellcmd = ["git", "--work-tree=%s" % reporoot, "reset", "--hard", "HEAD"]
521 runcmd(["sh", "-c", " ".join(shellcmd)], self.dir)
522 # Also need to take care of any stray untracked files
523 shellcmd = ["git", "--work-tree=%s" % reporoot, "clean", "-f"]
524 runcmd(["sh", "-c", " ".join(shellcmd)], self.dir)
525
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500526 # Fall back to git apply
527 shellcmd = ["git", "--git-dir=%s" % reporoot, "apply", "-p%s" % patch['strippath']]
528 try:
529 output = _applypatchhelper(shellcmd, patch, force, reverse, run)
530 except CmdError:
531 # Fall back to patch
532 output = PatchTree._applypatch(self, patch, force, reverse, run)
533 # Add all files
534 shellcmd = ["git", "add", "-f", "-A", "."]
535 output += runcmd(["sh", "-c", " ".join(shellcmd)], self.dir)
536 # Exclude the patches directory
537 shellcmd = ["git", "reset", "HEAD", self.patchdir]
538 output += runcmd(["sh", "-c", " ".join(shellcmd)], self.dir)
539 # Commit the result
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600540 (tmpfile, shellcmd) = self.prepareCommit(patch['file'], self.commituser, self.commitemail)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500541 try:
542 shellcmd.insert(0, patchfilevar)
543 output += runcmd(["sh", "-c", " ".join(shellcmd)], self.dir)
544 finally:
545 os.remove(tmpfile)
546 return output
547 finally:
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500548 shutil.rmtree(hooks_dir)
549 if os.path.lexists(hooks_dir_backup):
550 shutil.move(hooks_dir_backup, hooks_dir)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500551
552
553class QuiltTree(PatchSet):
554 def _runcmd(self, args, run = True):
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500555 quiltrc = self.d.getVar('QUILTRCFILE')
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500556 if not run:
557 return ["quilt"] + ["--quiltrc"] + [quiltrc] + args
558 runcmd(["quilt"] + ["--quiltrc"] + [quiltrc] + args, self.dir)
559
560 def _quiltpatchpath(self, file):
561 return os.path.join(self.dir, "patches", os.path.basename(file))
562
563
564 def __init__(self, dir, d):
565 PatchSet.__init__(self, dir, d)
566 self.initialized = False
567 p = os.path.join(self.dir, 'patches')
568 if not os.path.exists(p):
569 os.makedirs(p)
570
571 def Clean(self):
572 try:
573 self._runcmd(["pop", "-a", "-f"])
574 oe.path.remove(os.path.join(self.dir, "patches","series"))
575 except Exception:
576 pass
577 self.initialized = True
578
579 def InitFromDir(self):
580 # read series -> self.patches
581 seriespath = os.path.join(self.dir, 'patches', 'series')
582 if not os.path.exists(self.dir):
583 raise NotFoundError(self.dir)
584 if os.path.exists(seriespath):
585 with open(seriespath, 'r') as f:
586 for line in f.readlines():
587 patch = {}
588 parts = line.strip().split()
589 patch["quiltfile"] = self._quiltpatchpath(parts[0])
590 patch["quiltfilemd5"] = bb.utils.md5_file(patch["quiltfile"])
591 if len(parts) > 1:
592 patch["strippath"] = parts[1][2:]
593 self.patches.append(patch)
594
595 # determine which patches are applied -> self._current
596 try:
597 output = runcmd(["quilt", "applied"], self.dir)
598 except CmdError:
599 import sys
600 if sys.exc_value.output.strip() == "No patches applied":
601 return
602 else:
603 raise
604 output = [val for val in output.split('\n') if not val.startswith('#')]
605 for patch in self.patches:
606 if os.path.basename(patch["quiltfile"]) == output[-1]:
607 self._current = self.patches.index(patch)
608 self.initialized = True
609
610 def Import(self, patch, force = None):
611 if not self.initialized:
612 self.InitFromDir()
613 PatchSet.Import(self, patch, force)
614 oe.path.symlink(patch["file"], self._quiltpatchpath(patch["file"]), force=True)
615 with open(os.path.join(self.dir, "patches", "series"), "a") as f:
616 f.write(os.path.basename(patch["file"]) + " -p" + patch["strippath"] + "\n")
617 patch["quiltfile"] = self._quiltpatchpath(patch["file"])
618 patch["quiltfilemd5"] = bb.utils.md5_file(patch["quiltfile"])
619
620 # TODO: determine if the file being imported:
621 # 1) is already imported, and is the same
622 # 2) is already imported, but differs
623
624 self.patches.insert(self._current or 0, patch)
625
626
627 def Push(self, force = False, all = False, run = True):
628 # quilt push [-f]
629
630 args = ["push"]
631 if force:
632 args.append("-f")
633 if all:
634 args.append("-a")
635 if not run:
636 return self._runcmd(args, run)
637
638 self._runcmd(args)
639
640 if self._current is not None:
641 self._current = self._current + 1
642 else:
643 self._current = 0
644
645 def Pop(self, force = None, all = None):
646 # quilt pop [-f]
647 args = ["pop"]
648 if force:
649 args.append("-f")
650 if all:
651 args.append("-a")
652
653 self._runcmd(args)
654
655 if self._current == 0:
656 self._current = None
657
658 if self._current is not None:
659 self._current = self._current - 1
660
661 def Refresh(self, **kwargs):
662 if kwargs.get("remote"):
663 patch = self.patches[kwargs["patch"]]
664 if not patch:
665 raise PatchError("No patch found at index %s in patchset." % kwargs["patch"])
666 (type, host, path, user, pswd, parm) = bb.fetch.decodeurl(patch["remote"])
667 if type == "file":
668 import shutil
669 if not patch.get("file") and patch.get("remote"):
670 patch["file"] = bb.fetch2.localpath(patch["remote"], self.d)
671
672 shutil.copyfile(patch["quiltfile"], patch["file"])
673 else:
674 raise PatchError("Unable to do a remote refresh of %s, unsupported remote url scheme %s." % (os.path.basename(patch["quiltfile"]), type))
675 else:
676 # quilt refresh
677 args = ["refresh"]
678 if kwargs.get("quiltfile"):
679 args.append(os.path.basename(kwargs["quiltfile"]))
680 elif kwargs.get("patch"):
681 args.append(os.path.basename(self.patches[kwargs["patch"]]["quiltfile"]))
682 self._runcmd(args)
683
684class Resolver(object):
685 def __init__(self, patchset, terminal):
686 raise NotImplementedError()
687
688 def Resolve(self):
689 raise NotImplementedError()
690
691 def Revert(self):
692 raise NotImplementedError()
693
694 def Finalize(self):
695 raise NotImplementedError()
696
697class NOOPResolver(Resolver):
698 def __init__(self, patchset, terminal):
699 self.patchset = patchset
700 self.terminal = terminal
701
702 def Resolve(self):
703 olddir = os.path.abspath(os.curdir)
704 os.chdir(self.patchset.dir)
705 try:
706 self.patchset.Push()
707 except Exception:
708 import sys
709 os.chdir(olddir)
710 raise
711
712# Patch resolver which relies on the user doing all the work involved in the
713# resolution, with the exception of refreshing the remote copy of the patch
714# files (the urls).
715class UserResolver(Resolver):
716 def __init__(self, patchset, terminal):
717 self.patchset = patchset
718 self.terminal = terminal
719
720 # Force a push in the patchset, then drop to a shell for the user to
721 # resolve any rejected hunks
722 def Resolve(self):
723 olddir = os.path.abspath(os.curdir)
724 os.chdir(self.patchset.dir)
725 try:
726 self.patchset.Push(False)
727 except CmdError as v:
728 # Patch application failed
729 patchcmd = self.patchset.Push(True, False, False)
730
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500731 t = self.patchset.d.getVar('T')
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500732 if not t:
733 bb.msg.fatal("Build", "T not set")
734 bb.utils.mkdirhier(t)
735 import random
736 rcfile = "%s/bashrc.%s.%s" % (t, str(os.getpid()), random.random())
737 with open(rcfile, "w") as f:
738 f.write("echo '*** Manual patch resolution mode ***'\n")
739 f.write("echo 'Dropping to a shell, so patch rejects can be fixed manually.'\n")
740 f.write("echo 'Run \"quilt refresh\" when patch is corrected, press CTRL+D to exit.'\n")
741 f.write("echo ''\n")
742 f.write(" ".join(patchcmd) + "\n")
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600743 os.chmod(rcfile, 0o775)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500744
745 self.terminal("bash --rcfile " + rcfile, 'Patch Rejects: Please fix patch rejects manually', self.patchset.d)
746
747 # Construct a new PatchSet after the user's changes, compare the
748 # sets, checking patches for modifications, and doing a remote
749 # refresh on each.
750 oldpatchset = self.patchset
751 self.patchset = oldpatchset.__class__(self.patchset.dir, self.patchset.d)
752
753 for patch in self.patchset.patches:
754 oldpatch = None
755 for opatch in oldpatchset.patches:
756 if opatch["quiltfile"] == patch["quiltfile"]:
757 oldpatch = opatch
758
759 if oldpatch:
760 patch["remote"] = oldpatch["remote"]
761 if patch["quiltfile"] == oldpatch["quiltfile"]:
762 if patch["quiltfilemd5"] != oldpatch["quiltfilemd5"]:
763 bb.note("Patch %s has changed, updating remote url %s" % (os.path.basename(patch["quiltfile"]), patch["remote"]))
764 # user change? remote refresh
765 self.patchset.Refresh(remote=True, patch=self.patchset.patches.index(patch))
766 else:
767 # User did not fix the problem. Abort.
768 raise PatchError("Patch application failed, and user did not fix and refresh the patch.")
769 except Exception:
770 os.chdir(olddir)
771 raise
772 os.chdir(olddir)
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500773
774
775def patch_path(url, fetch, workdir, expand=True):
776 """Return the local path of a patch, or None if this isn't a patch"""
777
778 local = fetch.localpath(url)
779 base, ext = os.path.splitext(os.path.basename(local))
780 if ext in ('.gz', '.bz2', '.xz', '.Z'):
781 if expand:
782 local = os.path.join(workdir, base)
783 ext = os.path.splitext(base)[1]
784
785 urldata = fetch.ud[url]
786 if "apply" in urldata.parm:
787 apply = oe.types.boolean(urldata.parm["apply"])
788 if not apply:
789 return
790 elif ext not in (".diff", ".patch"):
791 return
792
793 return local
794
795def src_patches(d, all=False, expand=True):
796 workdir = d.getVar('WORKDIR')
797 fetch = bb.fetch2.Fetch([], d)
798 patches = []
799 sources = []
800 for url in fetch.urls:
801 local = patch_path(url, fetch, workdir, expand)
802 if not local:
803 if all:
804 local = fetch.localpath(url)
805 sources.append(local)
806 continue
807
808 urldata = fetch.ud[url]
809 parm = urldata.parm
810 patchname = parm.get('pname') or os.path.basename(local)
811
812 apply, reason = should_apply(parm, d)
813 if not apply:
814 if reason:
815 bb.note("Patch %s %s" % (patchname, reason))
816 continue
817
818 patchparm = {'patchname': patchname}
819 if "striplevel" in parm:
820 striplevel = parm["striplevel"]
821 elif "pnum" in parm:
822 #bb.msg.warn(None, "Deprecated usage of 'pnum' url parameter in '%s', please use 'striplevel'" % url)
823 striplevel = parm["pnum"]
824 else:
825 striplevel = '1'
826 patchparm['striplevel'] = striplevel
827
828 patchdir = parm.get('patchdir')
829 if patchdir:
830 patchparm['patchdir'] = patchdir
831
832 localurl = bb.fetch.encodeurl(('file', '', local, '', '', patchparm))
833 patches.append(localurl)
834
835 if all:
836 return sources
837
838 return patches
839
840
841def should_apply(parm, d):
842 if "mindate" in parm or "maxdate" in parm:
843 pn = d.getVar('PN')
844 srcdate = d.getVar('SRCDATE_%s' % pn)
845 if not srcdate:
846 srcdate = d.getVar('SRCDATE')
847
848 if srcdate == "now":
849 srcdate = d.getVar('DATE')
850
851 if "maxdate" in parm and parm["maxdate"] < srcdate:
852 return False, 'is outdated'
853
854 if "mindate" in parm and parm["mindate"] > srcdate:
855 return False, 'is predated'
856
857
858 if "minrev" in parm:
859 srcrev = d.getVar('SRCREV')
860 if srcrev and srcrev < parm["minrev"]:
861 return False, 'applies to later revisions'
862
863 if "maxrev" in parm:
864 srcrev = d.getVar('SRCREV')
865 if srcrev and srcrev > parm["maxrev"]:
866 return False, 'applies to earlier revisions'
867
868 if "rev" in parm:
869 srcrev = d.getVar('SRCREV')
870 if srcrev and parm["rev"] not in srcrev:
871 return False, "doesn't apply to revision"
872
873 if "notrev" in parm:
874 srcrev = d.getVar('SRCREV')
875 if srcrev and parm["notrev"] in srcrev:
876 return False, "doesn't apply to revision"
877
878 return True, None
879