blob: af7aa5235158daa82f1e52cad2abcd28003cc284 [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)
Brad Bishop316dfdd2018-06-25 12:45:53 -040039 if " fuzz " in output:
40 bb.warn("""
41Some of the context lines in patches were ignored. This can lead to incorrectly applied patches.
42The context lines in the patches can be updated with devtool:
43
44 devtool modify <recipe>
45 devtool finish --force-patch-refresh <recipe> <layer_path>
46
47Then the updated patches and the source tree (in devtool's workspace)
48should be reviewed to make sure the patches apply in the correct place
49and don't introduce duplicate lines (which can, and does happen
50when some of the context is ignored). Further information:
51http://lists.openembedded.org/pipermail/openembedded-core/2018-March/148675.html
52https://bugzilla.yoctoproject.org/show_bug.cgi?id=10450
53Details:
54{}""".format(output))
Patrick Williamsc124f4f2015-09-15 14:41:29 -050055 return output
56
57 finally:
58 if dir:
59 os.chdir(olddir)
60
61class PatchError(Exception):
62 def __init__(self, msg):
63 self.msg = msg
64
65 def __str__(self):
66 return "Patch Error: %s" % self.msg
67
68class PatchSet(object):
69 defaults = {
70 "strippath": 1
71 }
72
73 def __init__(self, dir, d):
74 self.dir = dir
75 self.d = d
76 self.patches = []
77 self._current = None
78
79 def current(self):
80 return self._current
81
82 def Clean(self):
83 """
84 Clean out the patch set. Generally includes unapplying all
85 patches and wiping out all associated metadata.
86 """
87 raise NotImplementedError()
88
89 def Import(self, patch, force):
90 if not patch.get("file"):
91 if not patch.get("remote"):
92 raise PatchError("Patch file must be specified in patch import.")
93 else:
94 patch["file"] = bb.fetch2.localpath(patch["remote"], self.d)
95
96 for param in PatchSet.defaults:
97 if not patch.get(param):
98 patch[param] = PatchSet.defaults[param]
99
100 if patch.get("remote"):
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500101 patch["file"] = self.d.expand(bb.fetch2.localpath(patch["remote"], self.d))
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500102
103 patch["filemd5"] = bb.utils.md5_file(patch["file"])
104
105 def Push(self, force):
106 raise NotImplementedError()
107
108 def Pop(self, force):
109 raise NotImplementedError()
110
111 def Refresh(self, remote = None, all = None):
112 raise NotImplementedError()
113
114 @staticmethod
115 def getPatchedFiles(patchfile, striplevel, srcdir=None):
116 """
117 Read a patch file and determine which files it will modify.
118 Params:
119 patchfile: the patch file to read
120 striplevel: the strip level at which the patch is going to be applied
121 srcdir: optional path to join onto the patched file paths
122 Returns:
123 A list of tuples of file path and change mode ('A' for add,
124 'D' for delete or 'M' for modify)
125 """
126
127 def patchedpath(patchline):
128 filepth = patchline.split()[1]
129 if filepth.endswith('/dev/null'):
130 return '/dev/null'
131 filesplit = filepth.split(os.sep)
132 if striplevel > len(filesplit):
133 bb.error('Patch %s has invalid strip level %d' % (patchfile, striplevel))
134 return None
135 return os.sep.join(filesplit[striplevel:])
136
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600137 for encoding in ['utf-8', 'latin-1']:
138 try:
139 copiedmode = False
140 filelist = []
141 with open(patchfile) as f:
142 for line in f:
143 if line.startswith('--- '):
144 patchpth = patchedpath(line)
145 if not patchpth:
146 break
147 if copiedmode:
148 addedfile = patchpth
149 else:
150 removedfile = patchpth
151 elif line.startswith('+++ '):
152 addedfile = patchedpath(line)
153 if not addedfile:
154 break
155 elif line.startswith('*** '):
156 copiedmode = True
157 removedfile = patchedpath(line)
158 if not removedfile:
159 break
160 else:
161 removedfile = None
162 addedfile = None
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500163
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600164 if addedfile and removedfile:
165 if removedfile == '/dev/null':
166 mode = 'A'
167 elif addedfile == '/dev/null':
168 mode = 'D'
169 else:
170 mode = 'M'
171 if srcdir:
172 fullpath = os.path.abspath(os.path.join(srcdir, addedfile))
173 else:
174 fullpath = addedfile
175 filelist.append((fullpath, mode))
176 except UnicodeDecodeError:
177 continue
178 break
179 else:
180 raise PatchError('Unable to decode %s' % patchfile)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500181
182 return filelist
183
184
185class PatchTree(PatchSet):
186 def __init__(self, dir, d):
187 PatchSet.__init__(self, dir, d)
188 self.patchdir = os.path.join(self.dir, 'patches')
189 self.seriespath = os.path.join(self.dir, 'patches', 'series')
190 bb.utils.mkdirhier(self.patchdir)
191
192 def _appendPatchFile(self, patch, strippath):
193 with open(self.seriespath, 'a') as f:
194 f.write(os.path.basename(patch) + "," + strippath + "\n")
195 shellcmd = ["cat", patch, ">" , self.patchdir + "/" + os.path.basename(patch)]
196 runcmd(["sh", "-c", " ".join(shellcmd)], self.dir)
197
198 def _removePatch(self, p):
199 patch = {}
200 patch['file'] = p.split(",")[0]
201 patch['strippath'] = p.split(",")[1]
202 self._applypatch(patch, False, True)
203
204 def _removePatchFile(self, all = False):
205 if not os.path.exists(self.seriespath):
206 return
207 with open(self.seriespath, 'r+') as f:
208 patches = f.readlines()
209 if all:
210 for p in reversed(patches):
211 self._removePatch(os.path.join(self.patchdir, p.strip()))
212 patches = []
213 else:
214 self._removePatch(os.path.join(self.patchdir, patches[-1].strip()))
215 patches.pop()
216 with open(self.seriespath, 'w') as f:
217 for p in patches:
218 f.write(p)
219
220 def Import(self, patch, force = None):
221 """"""
222 PatchSet.Import(self, patch, force)
223
224 if self._current is not None:
225 i = self._current + 1
226 else:
227 i = 0
228 self.patches.insert(i, patch)
229
230 def _applypatch(self, patch, force = False, reverse = False, run = True):
Brad Bishop316dfdd2018-06-25 12:45:53 -0400231 shellcmd = ["cat", patch['file'], "|", "patch", "--no-backup-if-mismatch", "-p", patch['strippath']]
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500232 if reverse:
233 shellcmd.append('-R')
234
235 if not run:
236 return "sh" + "-c" + " ".join(shellcmd)
237
238 if not force:
239 shellcmd.append('--dry-run')
240
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500241 try:
242 output = runcmd(["sh", "-c", " ".join(shellcmd)], self.dir)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500243
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500244 if force:
245 return
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500246
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500247 shellcmd.pop(len(shellcmd) - 1)
248 output = runcmd(["sh", "-c", " ".join(shellcmd)], self.dir)
249 except CmdError as err:
250 raise bb.BBHandledException("Applying '%s' failed:\n%s" %
251 (os.path.basename(patch['file']), err.output))
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500252
253 if not reverse:
254 self._appendPatchFile(patch['file'], patch['strippath'])
255
256 return output
257
258 def Push(self, force = False, all = False, run = True):
259 bb.note("self._current is %s" % self._current)
260 bb.note("patches is %s" % self.patches)
261 if all:
262 for i in self.patches:
263 bb.note("applying patch %s" % i)
264 self._applypatch(i, force)
265 self._current = i
266 else:
267 if self._current is not None:
268 next = self._current + 1
269 else:
270 next = 0
271
272 bb.note("applying patch %s" % self.patches[next])
273 ret = self._applypatch(self.patches[next], force)
274
275 self._current = next
276 return ret
277
278 def Pop(self, force = None, all = None):
279 if all:
280 self._removePatchFile(True)
281 self._current = None
282 else:
283 self._removePatchFile(False)
284
285 if self._current == 0:
286 self._current = None
287
288 if self._current is not None:
289 self._current = self._current - 1
290
291 def Clean(self):
292 """"""
293 self.Pop(all=True)
294
295class GitApplyTree(PatchTree):
296 patch_line_prefix = '%% original patch'
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500297 ignore_commit_prefix = '%% ignore'
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500298
299 def __init__(self, dir, d):
300 PatchTree.__init__(self, dir, d)
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500301 self.commituser = d.getVar('PATCH_GIT_USER_NAME')
302 self.commitemail = d.getVar('PATCH_GIT_USER_EMAIL')
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500303
304 @staticmethod
305 def extractPatchHeader(patchfile):
306 """
307 Extract just the header lines from the top of a patch file
308 """
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600309 for encoding in ['utf-8', 'latin-1']:
310 lines = []
311 try:
312 with open(patchfile, 'r', encoding=encoding) as f:
313 for line in f:
314 if line.startswith('Index: ') or line.startswith('diff -') or line.startswith('---'):
315 break
316 lines.append(line)
317 except UnicodeDecodeError:
318 continue
319 break
320 else:
321 raise PatchError('Unable to find a character encoding to decode %s' % patchfile)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500322 return lines
323
324 @staticmethod
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500325 def decodeAuthor(line):
326 from email.header import decode_header
327 authorval = line.split(':', 1)[1].strip().replace('"', '')
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600328 result = decode_header(authorval)[0][0]
329 if hasattr(result, 'decode'):
330 result = result.decode('utf-8')
331 return result
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500332
333 @staticmethod
334 def interpretPatchHeader(headerlines):
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500335 import re
336 author_re = re.compile('[\S ]+ <\S+@\S+\.\S+>')
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600337 from_commit_re = re.compile('^From [a-z0-9]{40} .*')
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500338 outlines = []
339 author = None
340 date = None
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500341 subject = None
342 for line in headerlines:
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500343 if line.startswith('Subject: '):
344 subject = line.split(':', 1)[1]
345 # Remove any [PATCH][oe-core] etc.
346 subject = re.sub(r'\[.+?\]\s*', '', subject)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500347 continue
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500348 elif line.startswith('From: ') or line.startswith('Author: '):
349 authorval = GitApplyTree.decodeAuthor(line)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500350 # git is fussy about author formatting i.e. it must be Name <email@domain>
351 if author_re.match(authorval):
352 author = authorval
353 continue
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500354 elif line.startswith('Date: '):
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500355 if date is None:
356 dateval = line.split(':', 1)[1].strip()
357 # Very crude check for date format, since git will blow up if it's not in the right
358 # format. Without e.g. a python-dateutils dependency we can't do a whole lot more
359 if len(dateval) > 12:
360 date = dateval
361 continue
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500362 elif not author and line.lower().startswith('signed-off-by: '):
363 authorval = GitApplyTree.decodeAuthor(line)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500364 # git is fussy about author formatting i.e. it must be Name <email@domain>
365 if author_re.match(authorval):
366 author = authorval
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600367 elif from_commit_re.match(line):
368 # We don't want the From <commit> line - if it's present it will break rebasing
369 continue
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500370 outlines.append(line)
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600371
372 if not subject:
373 firstline = None
374 for line in headerlines:
375 line = line.strip()
376 if firstline:
377 if line:
378 # Second line is not blank, the first line probably isn't usable
379 firstline = None
380 break
381 elif line:
382 firstline = line
383 if firstline and not firstline.startswith(('#', 'Index:', 'Upstream-Status:')) and len(firstline) < 100:
384 subject = firstline
385
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500386 return outlines, author, date, subject
387
388 @staticmethod
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600389 def gitCommandUserOptions(cmd, commituser=None, commitemail=None, d=None):
390 if d:
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500391 commituser = d.getVar('PATCH_GIT_USER_NAME')
392 commitemail = d.getVar('PATCH_GIT_USER_EMAIL')
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600393 if commituser:
394 cmd += ['-c', 'user.name="%s"' % commituser]
395 if commitemail:
396 cmd += ['-c', 'user.email="%s"' % commitemail]
397
398 @staticmethod
399 def prepareCommit(patchfile, commituser=None, commitemail=None):
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500400 """
401 Prepare a git commit command line based on the header from a patch file
402 (typically this is useful for patches that cannot be applied with "git am" due to formatting)
403 """
404 import tempfile
405 # Process patch header and extract useful information
406 lines = GitApplyTree.extractPatchHeader(patchfile)
407 outlines, author, date, subject = GitApplyTree.interpretPatchHeader(lines)
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600408 if not author or not subject or not date:
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500409 try:
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600410 shellcmd = ["git", "log", "--format=email", "--follow", "--diff-filter=A", "--", patchfile]
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500411 out = runcmd(["sh", "-c", " ".join(shellcmd)], os.path.dirname(patchfile))
412 except CmdError:
413 out = None
414 if out:
415 _, newauthor, newdate, newsubject = GitApplyTree.interpretPatchHeader(out.splitlines())
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600416 if not author:
417 # If we're setting the author then the date should be set as well
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500418 author = newauthor
419 date = newdate
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600420 elif not date:
421 # If we don't do this we'll get the current date, at least this will be closer
422 date = newdate
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500423 if not subject:
424 subject = newsubject
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600425 if subject and outlines and not outlines[0].strip() == subject:
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500426 outlines.insert(0, '%s\n\n' % subject.strip())
427
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500428 # Write out commit message to a file
429 with tempfile.NamedTemporaryFile('w', delete=False) as tf:
430 tmpfile = tf.name
431 for line in outlines:
432 tf.write(line)
433 # Prepare git command
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600434 cmd = ["git"]
435 GitApplyTree.gitCommandUserOptions(cmd, commituser, commitemail)
436 cmd += ["commit", "-F", tmpfile]
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500437 # git doesn't like plain email addresses as authors
438 if author and '<' in author:
439 cmd.append('--author="%s"' % author)
440 if date:
441 cmd.append('--date="%s"' % date)
442 return (tmpfile, cmd)
443
444 @staticmethod
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500445 def extractPatches(tree, startcommit, outdir, paths=None):
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500446 import tempfile
447 import shutil
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500448 import re
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500449 tempdir = tempfile.mkdtemp(prefix='oepatch')
450 try:
Brad Bishop316dfdd2018-06-25 12:45:53 -0400451 shellcmd = ["git", "format-patch", "--no-signature", "--no-numbered", startcommit, "-o", tempdir]
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500452 if paths:
453 shellcmd.append('--')
454 shellcmd.extend(paths)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500455 out = runcmd(["sh", "-c", " ".join(shellcmd)], tree)
456 if out:
457 for srcfile in out.split():
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600458 for encoding in ['utf-8', 'latin-1']:
459 patchlines = []
460 outfile = None
461 try:
462 with open(srcfile, 'r', encoding=encoding) as f:
463 for line in f:
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500464 checkline = line
465 if checkline.startswith('Subject: '):
466 checkline = re.sub(r'\[.+?\]\s*', '', checkline[9:])
467 if checkline.startswith(GitApplyTree.patch_line_prefix):
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600468 outfile = line.split()[-1].strip()
469 continue
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500470 if checkline.startswith(GitApplyTree.ignore_commit_prefix):
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600471 continue
472 patchlines.append(line)
473 except UnicodeDecodeError:
474 continue
475 break
476 else:
477 raise PatchError('Unable to find a character encoding to decode %s' % srcfile)
478
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500479 if not outfile:
480 outfile = os.path.basename(srcfile)
481 with open(os.path.join(outdir, outfile), 'w') as of:
482 for line in patchlines:
483 of.write(line)
484 finally:
485 shutil.rmtree(tempdir)
486
487 def _applypatch(self, patch, force = False, reverse = False, run = True):
488 import shutil
489
490 def _applypatchhelper(shellcmd, patch, force = False, reverse = False, run = True):
491 if reverse:
492 shellcmd.append('-R')
493
494 shellcmd.append(patch['file'])
495
496 if not run:
497 return "sh" + "-c" + " ".join(shellcmd)
498
499 return runcmd(["sh", "-c", " ".join(shellcmd)], self.dir)
500
501 # Add hooks which add a pointer to the original patch file name in the commit message
502 reporoot = (runcmd("git rev-parse --show-toplevel".split(), self.dir) or '').strip()
503 if not reporoot:
504 raise Exception("Cannot get repository root for directory %s" % self.dir)
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500505 hooks_dir = os.path.join(reporoot, '.git', 'hooks')
506 hooks_dir_backup = hooks_dir + '.devtool-orig'
507 if os.path.lexists(hooks_dir_backup):
508 raise Exception("Git hooks backup directory already exists: %s" % hooks_dir_backup)
509 if os.path.lexists(hooks_dir):
510 shutil.move(hooks_dir, hooks_dir_backup)
511 os.mkdir(hooks_dir)
512 commithook = os.path.join(hooks_dir, 'commit-msg')
513 applyhook = os.path.join(hooks_dir, 'applypatch-msg')
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500514 with open(commithook, 'w') as f:
515 # NOTE: the formatting here is significant; if you change it you'll also need to
516 # change other places which read it back
517 f.write('echo >> $1\n')
518 f.write('echo "%s: $PATCHFILE" >> $1\n' % GitApplyTree.patch_line_prefix)
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600519 os.chmod(commithook, 0o755)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500520 shutil.copy2(commithook, applyhook)
521 try:
522 patchfilevar = 'PATCHFILE="%s"' % os.path.basename(patch['file'])
523 try:
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600524 shellcmd = [patchfilevar, "git", "--work-tree=%s" % reporoot]
525 self.gitCommandUserOptions(shellcmd, self.commituser, self.commitemail)
526 shellcmd += ["am", "-3", "--keep-cr", "-p%s" % patch['strippath']]
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500527 return _applypatchhelper(shellcmd, patch, force, reverse, run)
528 except CmdError:
529 # Need to abort the git am, or we'll still be within it at the end
530 try:
531 shellcmd = ["git", "--work-tree=%s" % reporoot, "am", "--abort"]
532 runcmd(["sh", "-c", " ".join(shellcmd)], self.dir)
533 except CmdError:
534 pass
Patrick Williamsf1e5d692016-03-30 15:21:19 -0500535 # git am won't always clean up after itself, sadly, so...
536 shellcmd = ["git", "--work-tree=%s" % reporoot, "reset", "--hard", "HEAD"]
537 runcmd(["sh", "-c", " ".join(shellcmd)], self.dir)
538 # Also need to take care of any stray untracked files
539 shellcmd = ["git", "--work-tree=%s" % reporoot, "clean", "-f"]
540 runcmd(["sh", "-c", " ".join(shellcmd)], self.dir)
541
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500542 # Fall back to git apply
543 shellcmd = ["git", "--git-dir=%s" % reporoot, "apply", "-p%s" % patch['strippath']]
544 try:
545 output = _applypatchhelper(shellcmd, patch, force, reverse, run)
546 except CmdError:
547 # Fall back to patch
548 output = PatchTree._applypatch(self, patch, force, reverse, run)
549 # Add all files
550 shellcmd = ["git", "add", "-f", "-A", "."]
551 output += runcmd(["sh", "-c", " ".join(shellcmd)], self.dir)
552 # Exclude the patches directory
553 shellcmd = ["git", "reset", "HEAD", self.patchdir]
554 output += runcmd(["sh", "-c", " ".join(shellcmd)], self.dir)
555 # Commit the result
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600556 (tmpfile, shellcmd) = self.prepareCommit(patch['file'], self.commituser, self.commitemail)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500557 try:
558 shellcmd.insert(0, patchfilevar)
559 output += runcmd(["sh", "-c", " ".join(shellcmd)], self.dir)
560 finally:
561 os.remove(tmpfile)
562 return output
563 finally:
Patrick Williamsd8c66bc2016-06-20 12:57:21 -0500564 shutil.rmtree(hooks_dir)
565 if os.path.lexists(hooks_dir_backup):
566 shutil.move(hooks_dir_backup, hooks_dir)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500567
568
569class QuiltTree(PatchSet):
570 def _runcmd(self, args, run = True):
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500571 quiltrc = self.d.getVar('QUILTRCFILE')
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500572 if not run:
573 return ["quilt"] + ["--quiltrc"] + [quiltrc] + args
574 runcmd(["quilt"] + ["--quiltrc"] + [quiltrc] + args, self.dir)
575
576 def _quiltpatchpath(self, file):
577 return os.path.join(self.dir, "patches", os.path.basename(file))
578
579
580 def __init__(self, dir, d):
581 PatchSet.__init__(self, dir, d)
582 self.initialized = False
583 p = os.path.join(self.dir, 'patches')
584 if not os.path.exists(p):
585 os.makedirs(p)
586
587 def Clean(self):
588 try:
589 self._runcmd(["pop", "-a", "-f"])
590 oe.path.remove(os.path.join(self.dir, "patches","series"))
591 except Exception:
592 pass
593 self.initialized = True
594
595 def InitFromDir(self):
596 # read series -> self.patches
597 seriespath = os.path.join(self.dir, 'patches', 'series')
598 if not os.path.exists(self.dir):
599 raise NotFoundError(self.dir)
600 if os.path.exists(seriespath):
601 with open(seriespath, 'r') as f:
602 for line in f.readlines():
603 patch = {}
604 parts = line.strip().split()
605 patch["quiltfile"] = self._quiltpatchpath(parts[0])
606 patch["quiltfilemd5"] = bb.utils.md5_file(patch["quiltfile"])
607 if len(parts) > 1:
608 patch["strippath"] = parts[1][2:]
609 self.patches.append(patch)
610
611 # determine which patches are applied -> self._current
612 try:
613 output = runcmd(["quilt", "applied"], self.dir)
614 except CmdError:
615 import sys
616 if sys.exc_value.output.strip() == "No patches applied":
617 return
618 else:
619 raise
620 output = [val for val in output.split('\n') if not val.startswith('#')]
621 for patch in self.patches:
622 if os.path.basename(patch["quiltfile"]) == output[-1]:
623 self._current = self.patches.index(patch)
624 self.initialized = True
625
626 def Import(self, patch, force = None):
627 if not self.initialized:
628 self.InitFromDir()
629 PatchSet.Import(self, patch, force)
630 oe.path.symlink(patch["file"], self._quiltpatchpath(patch["file"]), force=True)
631 with open(os.path.join(self.dir, "patches", "series"), "a") as f:
632 f.write(os.path.basename(patch["file"]) + " -p" + patch["strippath"] + "\n")
633 patch["quiltfile"] = self._quiltpatchpath(patch["file"])
634 patch["quiltfilemd5"] = bb.utils.md5_file(patch["quiltfile"])
635
636 # TODO: determine if the file being imported:
637 # 1) is already imported, and is the same
638 # 2) is already imported, but differs
639
640 self.patches.insert(self._current or 0, patch)
641
642
643 def Push(self, force = False, all = False, run = True):
644 # quilt push [-f]
645
646 args = ["push"]
647 if force:
648 args.append("-f")
649 if all:
650 args.append("-a")
651 if not run:
652 return self._runcmd(args, run)
653
654 self._runcmd(args)
655
656 if self._current is not None:
657 self._current = self._current + 1
658 else:
659 self._current = 0
660
661 def Pop(self, force = None, all = None):
662 # quilt pop [-f]
663 args = ["pop"]
664 if force:
665 args.append("-f")
666 if all:
667 args.append("-a")
668
669 self._runcmd(args)
670
671 if self._current == 0:
672 self._current = None
673
674 if self._current is not None:
675 self._current = self._current - 1
676
677 def Refresh(self, **kwargs):
678 if kwargs.get("remote"):
679 patch = self.patches[kwargs["patch"]]
680 if not patch:
681 raise PatchError("No patch found at index %s in patchset." % kwargs["patch"])
682 (type, host, path, user, pswd, parm) = bb.fetch.decodeurl(patch["remote"])
683 if type == "file":
684 import shutil
685 if not patch.get("file") and patch.get("remote"):
686 patch["file"] = bb.fetch2.localpath(patch["remote"], self.d)
687
688 shutil.copyfile(patch["quiltfile"], patch["file"])
689 else:
690 raise PatchError("Unable to do a remote refresh of %s, unsupported remote url scheme %s." % (os.path.basename(patch["quiltfile"]), type))
691 else:
692 # quilt refresh
693 args = ["refresh"]
694 if kwargs.get("quiltfile"):
695 args.append(os.path.basename(kwargs["quiltfile"]))
696 elif kwargs.get("patch"):
697 args.append(os.path.basename(self.patches[kwargs["patch"]]["quiltfile"]))
698 self._runcmd(args)
699
700class Resolver(object):
701 def __init__(self, patchset, terminal):
702 raise NotImplementedError()
703
704 def Resolve(self):
705 raise NotImplementedError()
706
707 def Revert(self):
708 raise NotImplementedError()
709
710 def Finalize(self):
711 raise NotImplementedError()
712
713class NOOPResolver(Resolver):
714 def __init__(self, patchset, terminal):
715 self.patchset = patchset
716 self.terminal = terminal
717
718 def Resolve(self):
719 olddir = os.path.abspath(os.curdir)
720 os.chdir(self.patchset.dir)
721 try:
722 self.patchset.Push()
723 except Exception:
724 import sys
725 os.chdir(olddir)
726 raise
727
728# Patch resolver which relies on the user doing all the work involved in the
729# resolution, with the exception of refreshing the remote copy of the patch
730# files (the urls).
731class UserResolver(Resolver):
732 def __init__(self, patchset, terminal):
733 self.patchset = patchset
734 self.terminal = terminal
735
736 # Force a push in the patchset, then drop to a shell for the user to
737 # resolve any rejected hunks
738 def Resolve(self):
739 olddir = os.path.abspath(os.curdir)
740 os.chdir(self.patchset.dir)
741 try:
742 self.patchset.Push(False)
743 except CmdError as v:
744 # Patch application failed
745 patchcmd = self.patchset.Push(True, False, False)
746
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500747 t = self.patchset.d.getVar('T')
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500748 if not t:
749 bb.msg.fatal("Build", "T not set")
750 bb.utils.mkdirhier(t)
751 import random
752 rcfile = "%s/bashrc.%s.%s" % (t, str(os.getpid()), random.random())
753 with open(rcfile, "w") as f:
754 f.write("echo '*** Manual patch resolution mode ***'\n")
755 f.write("echo 'Dropping to a shell, so patch rejects can be fixed manually.'\n")
756 f.write("echo 'Run \"quilt refresh\" when patch is corrected, press CTRL+D to exit.'\n")
757 f.write("echo ''\n")
758 f.write(" ".join(patchcmd) + "\n")
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600759 os.chmod(rcfile, 0o775)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500760
761 self.terminal("bash --rcfile " + rcfile, 'Patch Rejects: Please fix patch rejects manually', self.patchset.d)
762
763 # Construct a new PatchSet after the user's changes, compare the
764 # sets, checking patches for modifications, and doing a remote
765 # refresh on each.
766 oldpatchset = self.patchset
767 self.patchset = oldpatchset.__class__(self.patchset.dir, self.patchset.d)
768
769 for patch in self.patchset.patches:
770 oldpatch = None
771 for opatch in oldpatchset.patches:
772 if opatch["quiltfile"] == patch["quiltfile"]:
773 oldpatch = opatch
774
775 if oldpatch:
776 patch["remote"] = oldpatch["remote"]
777 if patch["quiltfile"] == oldpatch["quiltfile"]:
778 if patch["quiltfilemd5"] != oldpatch["quiltfilemd5"]:
779 bb.note("Patch %s has changed, updating remote url %s" % (os.path.basename(patch["quiltfile"]), patch["remote"]))
780 # user change? remote refresh
781 self.patchset.Refresh(remote=True, patch=self.patchset.patches.index(patch))
782 else:
783 # User did not fix the problem. Abort.
784 raise PatchError("Patch application failed, and user did not fix and refresh the patch.")
785 except Exception:
786 os.chdir(olddir)
787 raise
788 os.chdir(olddir)
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500789
790
791def patch_path(url, fetch, workdir, expand=True):
792 """Return the local path of a patch, or None if this isn't a patch"""
793
794 local = fetch.localpath(url)
795 base, ext = os.path.splitext(os.path.basename(local))
796 if ext in ('.gz', '.bz2', '.xz', '.Z'):
797 if expand:
798 local = os.path.join(workdir, base)
799 ext = os.path.splitext(base)[1]
800
801 urldata = fetch.ud[url]
802 if "apply" in urldata.parm:
803 apply = oe.types.boolean(urldata.parm["apply"])
804 if not apply:
805 return
806 elif ext not in (".diff", ".patch"):
807 return
808
809 return local
810
811def src_patches(d, all=False, expand=True):
812 workdir = d.getVar('WORKDIR')
813 fetch = bb.fetch2.Fetch([], d)
814 patches = []
815 sources = []
816 for url in fetch.urls:
817 local = patch_path(url, fetch, workdir, expand)
818 if not local:
819 if all:
820 local = fetch.localpath(url)
821 sources.append(local)
822 continue
823
824 urldata = fetch.ud[url]
825 parm = urldata.parm
826 patchname = parm.get('pname') or os.path.basename(local)
827
828 apply, reason = should_apply(parm, d)
829 if not apply:
830 if reason:
831 bb.note("Patch %s %s" % (patchname, reason))
832 continue
833
834 patchparm = {'patchname': patchname}
835 if "striplevel" in parm:
836 striplevel = parm["striplevel"]
837 elif "pnum" in parm:
838 #bb.msg.warn(None, "Deprecated usage of 'pnum' url parameter in '%s', please use 'striplevel'" % url)
839 striplevel = parm["pnum"]
840 else:
841 striplevel = '1'
842 patchparm['striplevel'] = striplevel
843
844 patchdir = parm.get('patchdir')
845 if patchdir:
846 patchparm['patchdir'] = patchdir
847
848 localurl = bb.fetch.encodeurl(('file', '', local, '', '', patchparm))
849 patches.append(localurl)
850
851 if all:
852 return sources
853
854 return patches
855
856
857def should_apply(parm, d):
858 if "mindate" in parm or "maxdate" in parm:
859 pn = d.getVar('PN')
860 srcdate = d.getVar('SRCDATE_%s' % pn)
861 if not srcdate:
862 srcdate = d.getVar('SRCDATE')
863
864 if srcdate == "now":
865 srcdate = d.getVar('DATE')
866
867 if "maxdate" in parm and parm["maxdate"] < srcdate:
868 return False, 'is outdated'
869
870 if "mindate" in parm and parm["mindate"] > srcdate:
871 return False, 'is predated'
872
873
874 if "minrev" in parm:
875 srcrev = d.getVar('SRCREV')
876 if srcrev and srcrev < parm["minrev"]:
877 return False, 'applies to later revisions'
878
879 if "maxrev" in parm:
880 srcrev = d.getVar('SRCREV')
881 if srcrev and srcrev > parm["maxrev"]:
882 return False, 'applies to earlier revisions'
883
884 if "rev" in parm:
885 srcrev = d.getVar('SRCREV')
886 if srcrev and parm["rev"] not in srcrev:
887 return False, "doesn't apply to revision"
888
889 if "notrev" in parm:
890 srcrev = d.getVar('SRCREV')
891 if srcrev and parm["notrev"] in srcrev:
892 return False, "doesn't apply to revision"
893
894 return True, None
895