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 import subprocess 25 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) 37 (exitstatus, output) = subprocess.getstatusoutput(cmd) 38 if exitstatus != 0: 39 raise CmdError(cmd, exitstatus >> 8, output) 40 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)) 56 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"): 102 patch["file"] = self.d.expand(bb.fetch2.localpath(patch["remote"], self.d)) 103 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 138 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 164 165 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) 182 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): 232 shellcmd = ["cat", patch['file'], "|", "patch", "--no-backup-if-mismatch", "-p", patch['strippath']] 233 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 242 try: 243 output = runcmd(["sh", "-c", " ".join(shellcmd)], self.dir) 244 245 if force: 246 return 247 248 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)) 253 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' 298 ignore_commit_prefix = '%% ignore' 299 300 def __init__(self, dir, d): 301 PatchTree.__init__(self, dir, d) 302 self.commituser = d.getVar('PATCH_GIT_USER_NAME') 303 self.commitemail = d.getVar('PATCH_GIT_USER_EMAIL') 304 305 @staticmethod 306 def extractPatchHeader(patchfile): 307 """ 308 Extract just the header lines from the top of a patch file 309 """ 310 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) 323 return lines 324 325 @staticmethod 326 def decodeAuthor(line): 327 from email.header import decode_header 328 authorval = line.split(':', 1)[1].strip().replace('"', '') 329 result = decode_header(authorval)[0][0] 330 if hasattr(result, 'decode'): 331 result = result.decode('utf-8') 332 return result 333 334 @staticmethod 335 def interpretPatchHeader(headerlines): 336 import re 337 author_re = re.compile('[\S ]+ <\S+@\S+\.\S+>') 338 from_commit_re = re.compile('^From [a-z0-9]{40} .*') 339 outlines = [] 340 author = None 341 date = None 342 subject = None 343 for line in headerlines: 344 if line.startswith('Subject: '): 345 subject = line.split(':', 1)[1] 346 # Remove any [PATCH][oe-core] etc. 347 subject = re.sub(r'\[.+?\]\s*', '', subject) 348 continue 349 elif line.startswith('From: ') or line.startswith('Author: '): 350 authorval = GitApplyTree.decodeAuthor(line) 351 # 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 355 elif line.startswith('Date: '): 356 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 363 elif not author and line.lower().startswith('signed-off-by: '): 364 authorval = GitApplyTree.decodeAuthor(line) 365 # git is fussy about author formatting i.e. it must be Name <email@domain> 366 if author_re.match(authorval): 367 author = authorval 368 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 371 outlines.append(line) 372 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 387 return outlines, author, date, subject 388 389 @staticmethod 390 def gitCommandUserOptions(cmd, commituser=None, commitemail=None, d=None): 391 if d: 392 commituser = d.getVar('PATCH_GIT_USER_NAME') 393 commitemail = d.getVar('PATCH_GIT_USER_EMAIL') 394 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): 401 """ 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) 409 if not author or not subject or not date: 410 try: 411 shellcmd = ["git", "log", "--format=email", "--follow", "--diff-filter=A", "--", patchfile] 412 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()) 417 if not author: 418 # If we're setting the author then the date should be set as well 419 author = newauthor 420 date = newdate 421 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 424 if not subject: 425 subject = newsubject 426 if subject and outlines and not outlines[0].strip() == subject: 427 outlines.insert(0, '%s\n\n' % subject.strip()) 428 429 # 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 435 cmd = ["git"] 436 GitApplyTree.gitCommandUserOptions(cmd, commituser, commitemail) 437 cmd += ["commit", "-F", tmpfile] 438 # 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 446 def extractPatches(tree, startcommit, outdir, paths=None): 447 import tempfile 448 import shutil 449 import re 450 tempdir = tempfile.mkdtemp(prefix='oepatch') 451 try: 452 shellcmd = ["git", "format-patch", "--no-signature", "--no-numbered", startcommit, "-o", tempdir] 453 if paths: 454 shellcmd.append('--') 455 shellcmd.extend(paths) 456 out = runcmd(["sh", "-c", " ".join(shellcmd)], tree) 457 if out: 458 for srcfile in out.split(): 459 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: 465 checkline = line 466 if checkline.startswith('Subject: '): 467 checkline = re.sub(r'\[.+?\]\s*', '', checkline[9:]) 468 if checkline.startswith(GitApplyTree.patch_line_prefix): 469 outfile = line.split()[-1].strip() 470 continue 471 if checkline.startswith(GitApplyTree.ignore_commit_prefix): 472 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 480 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) 506 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') 515 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) 520 os.chmod(commithook, 0o755) 521 shutil.copy2(commithook, applyhook) 522 try: 523 patchfilevar = 'PATCHFILE="%s"' % os.path.basename(patch['file']) 524 try: 525 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']] 528 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 536 # 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 543 # 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 557 (tmpfile, shellcmd) = self.prepareCommit(patch['file'], self.commituser, self.commitemail) 558 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: 565 shutil.rmtree(hooks_dir) 566 if os.path.lexists(hooks_dir_backup): 567 shutil.move(hooks_dir_backup, hooks_dir) 568 569 570class QuiltTree(PatchSet): 571 def _runcmd(self, args, run = True): 572 quiltrc = self.d.getVar('QUILTRCFILE') 573 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 748 t = self.patchset.d.getVar('T') 749 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") 760 os.chmod(rcfile, 0o775) 761 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) 790 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 897