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