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