1import oe.path 2import oe.types 3 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): 12 def __init__(self, command, exitstatus, output): 13 self.command = command 14 self.status = exitstatus 15 self.output = output 16 17 def __str__(self): 18 return "Command Error: '%s' exited with %d Output:\n%s" % \ 19 (self.command, self.status, self.output) 20 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: 38 raise CmdError(cmd, exitstatus >> 8, output) 39 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)) 55 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"): 101 patch["file"] = self.d.expand(bb.fetch2.localpath(patch["remote"], self.d)) 102 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 137 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 163 164 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) 181 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): 231 shellcmd = ["cat", patch['file'], "|", "patch", "--no-backup-if-mismatch", "-p", patch['strippath']] 232 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 241 try: 242 output = runcmd(["sh", "-c", " ".join(shellcmd)], self.dir) 243 244 if force: 245 return 246 247 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)) 252 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' 297 ignore_commit_prefix = '%% ignore' 298 299 def __init__(self, dir, d): 300 PatchTree.__init__(self, dir, d) 301 self.commituser = d.getVar('PATCH_GIT_USER_NAME') 302 self.commitemail = d.getVar('PATCH_GIT_USER_EMAIL') 303 304 @staticmethod 305 def extractPatchHeader(patchfile): 306 """ 307 Extract just the header lines from the top of a patch file 308 """ 309 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) 322 return lines 323 324 @staticmethod 325 def decodeAuthor(line): 326 from email.header import decode_header 327 authorval = line.split(':', 1)[1].strip().replace('"', '') 328 result = decode_header(authorval)[0][0] 329 if hasattr(result, 'decode'): 330 result = result.decode('utf-8') 331 return result 332 333 @staticmethod 334 def interpretPatchHeader(headerlines): 335 import re 336 author_re = re.compile('[\S ]+ <\S+@\S+\.\S+>') 337 from_commit_re = re.compile('^From [a-z0-9]{40} .*') 338 outlines = [] 339 author = None 340 date = None 341 subject = None 342 for line in headerlines: 343 if line.startswith('Subject: '): 344 subject = line.split(':', 1)[1] 345 # Remove any [PATCH][oe-core] etc. 346 subject = re.sub(r'\[.+?\]\s*', '', subject) 347 continue 348 elif line.startswith('From: ') or line.startswith('Author: '): 349 authorval = GitApplyTree.decodeAuthor(line) 350 # 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 354 elif line.startswith('Date: '): 355 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 362 elif not author and line.lower().startswith('signed-off-by: '): 363 authorval = GitApplyTree.decodeAuthor(line) 364 # git is fussy about author formatting i.e. it must be Name <email@domain> 365 if author_re.match(authorval): 366 author = authorval 367 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 370 outlines.append(line) 371 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 386 return outlines, author, date, subject 387 388 @staticmethod 389 def gitCommandUserOptions(cmd, commituser=None, commitemail=None, d=None): 390 if d: 391 commituser = d.getVar('PATCH_GIT_USER_NAME') 392 commitemail = d.getVar('PATCH_GIT_USER_EMAIL') 393 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): 400 """ 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) 408 if not author or not subject or not date: 409 try: 410 shellcmd = ["git", "log", "--format=email", "--follow", "--diff-filter=A", "--", patchfile] 411 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()) 416 if not author: 417 # If we're setting the author then the date should be set as well 418 author = newauthor 419 date = newdate 420 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 423 if not subject: 424 subject = newsubject 425 if subject and outlines and not outlines[0].strip() == subject: 426 outlines.insert(0, '%s\n\n' % subject.strip()) 427 428 # 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 434 cmd = ["git"] 435 GitApplyTree.gitCommandUserOptions(cmd, commituser, commitemail) 436 cmd += ["commit", "-F", tmpfile] 437 # 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 445 def extractPatches(tree, startcommit, outdir, paths=None): 446 import tempfile 447 import shutil 448 import re 449 tempdir = tempfile.mkdtemp(prefix='oepatch') 450 try: 451 shellcmd = ["git", "format-patch", "--no-signature", "--no-numbered", startcommit, "-o", tempdir] 452 if paths: 453 shellcmd.append('--') 454 shellcmd.extend(paths) 455 out = runcmd(["sh", "-c", " ".join(shellcmd)], tree) 456 if out: 457 for srcfile in out.split(): 458 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: 464 checkline = line 465 if checkline.startswith('Subject: '): 466 checkline = re.sub(r'\[.+?\]\s*', '', checkline[9:]) 467 if checkline.startswith(GitApplyTree.patch_line_prefix): 468 outfile = line.split()[-1].strip() 469 continue 470 if checkline.startswith(GitApplyTree.ignore_commit_prefix): 471 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 479 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) 505 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') 514 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) 519 os.chmod(commithook, 0o755) 520 shutil.copy2(commithook, applyhook) 521 try: 522 patchfilevar = 'PATCHFILE="%s"' % os.path.basename(patch['file']) 523 try: 524 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']] 527 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 535 # 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 542 # 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 556 (tmpfile, shellcmd) = self.prepareCommit(patch['file'], self.commituser, self.commitemail) 557 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: 564 shutil.rmtree(hooks_dir) 565 if os.path.lexists(hooks_dir_backup): 566 shutil.move(hooks_dir_backup, hooks_dir) 567 568 569class QuiltTree(PatchSet): 570 def _runcmd(self, args, run = True): 571 quiltrc = self.d.getVar('QUILTRCFILE') 572 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 747 t = self.patchset.d.getVar('T') 748 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") 759 os.chmod(rcfile, 0o775) 760 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) 789 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 896