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