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