1# 2# Copyright OpenEmbedded Contributors 3# 4# SPDX-License-Identifier: GPL-2.0-only 5# 6 7import os 8import shlex 9import subprocess 10import oe.path 11import oe.types 12 13class NotFoundError(bb.BBHandledException): 14 def __init__(self, path): 15 self.path = path 16 17 def __str__(self): 18 return "Error: %s not found." % self.path 19 20class CmdError(bb.BBHandledException): 21 def __init__(self, command, exitstatus, output): 22 self.command = command 23 self.status = exitstatus 24 self.output = output 25 26 def __str__(self): 27 return "Command Error: '%s' exited with %d Output:\n%s" % \ 28 (self.command, self.status, self.output) 29 30 31def runcmd(args, dir = None): 32 if dir: 33 olddir = os.path.abspath(os.curdir) 34 if not os.path.exists(dir): 35 raise NotFoundError(dir) 36 os.chdir(dir) 37 # print("cwd: %s -> %s" % (olddir, dir)) 38 39 try: 40 args = [ shlex.quote(str(arg)) for arg in args ] 41 cmd = " ".join(args) 42 # print("cmd: %s" % cmd) 43 proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True) 44 stdout, stderr = proc.communicate() 45 stdout = stdout.decode('utf-8') 46 stderr = stderr.decode('utf-8') 47 exitstatus = proc.returncode 48 if exitstatus != 0: 49 raise CmdError(cmd, exitstatus >> 8, "stdout: %s\nstderr: %s" % (stdout, stderr)) 50 if " fuzz " in stdout and "Hunk " in stdout: 51 # Drop patch fuzz info with header and footer to log file so 52 # insane.bbclass can handle to throw error/warning 53 bb.note("--- Patch fuzz start ---\n%s\n--- Patch fuzz end ---" % format(stdout)) 54 55 return stdout 56 57 finally: 58 if dir: 59 os.chdir(olddir) 60 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 notes_ref = "refs/notes/devtool" 298 original_patch = 'original patch' 299 ignore_commit = 'ignore' 300 301 def __init__(self, dir, d): 302 PatchTree.__init__(self, dir, d) 303 self.commituser = d.getVar('PATCH_GIT_USER_NAME') 304 self.commitemail = d.getVar('PATCH_GIT_USER_EMAIL') 305 if not self._isInitialized(d): 306 self._initRepo() 307 308 def _isInitialized(self, d): 309 cmd = "git rev-parse --show-toplevel" 310 try: 311 output = runcmd(cmd.split(), self.dir).strip() 312 except CmdError as err: 313 ## runcmd returned non-zero which most likely means 128 314 ## Not a git directory 315 return False 316 ## Make sure repo is in builddir to not break top-level git repos, or under workdir 317 return os.path.samefile(output, self.dir) or oe.path.is_path_parent(d.getVar('WORKDIR'), output) 318 319 def _initRepo(self): 320 runcmd("git init".split(), self.dir) 321 runcmd("git add .".split(), self.dir) 322 runcmd("git commit -a --allow-empty -m bitbake_patching_started".split(), self.dir) 323 324 @staticmethod 325 def extractPatchHeader(patchfile): 326 """ 327 Extract just the header lines from the top of a patch file 328 """ 329 for encoding in ['utf-8', 'latin-1']: 330 lines = [] 331 try: 332 with open(patchfile, 'r', encoding=encoding) as f: 333 for line in f: 334 if line.startswith('Index: ') or line.startswith('diff -') or line.startswith('---'): 335 break 336 lines.append(line) 337 except UnicodeDecodeError: 338 continue 339 break 340 else: 341 raise PatchError('Unable to find a character encoding to decode %s' % patchfile) 342 return lines 343 344 @staticmethod 345 def decodeAuthor(line): 346 from email.header import decode_header 347 authorval = line.split(':', 1)[1].strip().replace('"', '') 348 result = decode_header(authorval)[0][0] 349 if hasattr(result, 'decode'): 350 result = result.decode('utf-8') 351 return result 352 353 @staticmethod 354 def interpretPatchHeader(headerlines): 355 import re 356 author_re = re.compile(r'[\S ]+ <\S+@\S+\.\S+>') 357 from_commit_re = re.compile(r'^From [a-z0-9]{40} .*') 358 outlines = [] 359 author = None 360 date = None 361 subject = None 362 for line in headerlines: 363 if line.startswith('Subject: '): 364 subject = line.split(':', 1)[1] 365 # Remove any [PATCH][oe-core] etc. 366 subject = re.sub(r'\[.+?\]\s*', '', subject) 367 continue 368 elif line.startswith('From: ') or line.startswith('Author: '): 369 authorval = GitApplyTree.decodeAuthor(line) 370 # git is fussy about author formatting i.e. it must be Name <email@domain> 371 if author_re.match(authorval): 372 author = authorval 373 continue 374 elif line.startswith('Date: '): 375 if date is None: 376 dateval = line.split(':', 1)[1].strip() 377 # Very crude check for date format, since git will blow up if it's not in the right 378 # format. Without e.g. a python-dateutils dependency we can't do a whole lot more 379 if len(dateval) > 12: 380 date = dateval 381 continue 382 elif not author and line.lower().startswith('signed-off-by: '): 383 authorval = GitApplyTree.decodeAuthor(line) 384 # git is fussy about author formatting i.e. it must be Name <email@domain> 385 if author_re.match(authorval): 386 author = authorval 387 elif from_commit_re.match(line): 388 # We don't want the From <commit> line - if it's present it will break rebasing 389 continue 390 outlines.append(line) 391 392 if not subject: 393 firstline = None 394 for line in headerlines: 395 line = line.strip() 396 if firstline: 397 if line: 398 # Second line is not blank, the first line probably isn't usable 399 firstline = None 400 break 401 elif line: 402 firstline = line 403 if firstline and not firstline.startswith(('#', 'Index:', 'Upstream-Status:')) and len(firstline) < 100: 404 subject = firstline 405 406 return outlines, author, date, subject 407 408 @staticmethod 409 def gitCommandUserOptions(cmd, commituser=None, commitemail=None, d=None): 410 if d: 411 commituser = d.getVar('PATCH_GIT_USER_NAME') 412 commitemail = d.getVar('PATCH_GIT_USER_EMAIL') 413 if commituser: 414 cmd += ['-c', 'user.name="%s"' % commituser] 415 if commitemail: 416 cmd += ['-c', 'user.email="%s"' % commitemail] 417 418 @staticmethod 419 def prepareCommit(patchfile, commituser=None, commitemail=None): 420 """ 421 Prepare a git commit command line based on the header from a patch file 422 (typically this is useful for patches that cannot be applied with "git am" due to formatting) 423 """ 424 import tempfile 425 # Process patch header and extract useful information 426 lines = GitApplyTree.extractPatchHeader(patchfile) 427 outlines, author, date, subject = GitApplyTree.interpretPatchHeader(lines) 428 if not author or not subject or not date: 429 try: 430 shellcmd = ["git", "log", "--format=email", "--follow", "--diff-filter=A", "--", patchfile] 431 out = runcmd(["sh", "-c", " ".join(shellcmd)], os.path.dirname(patchfile)) 432 except CmdError: 433 out = None 434 if out: 435 _, newauthor, newdate, newsubject = GitApplyTree.interpretPatchHeader(out.splitlines()) 436 if not author: 437 # If we're setting the author then the date should be set as well 438 author = newauthor 439 date = newdate 440 elif not date: 441 # If we don't do this we'll get the current date, at least this will be closer 442 date = newdate 443 if not subject: 444 subject = newsubject 445 if subject and not (outlines and outlines[0].strip() == subject): 446 outlines.insert(0, '%s\n\n' % subject.strip()) 447 448 # Write out commit message to a file 449 with tempfile.NamedTemporaryFile('w', delete=False) as tf: 450 tmpfile = tf.name 451 for line in outlines: 452 tf.write(line) 453 # Prepare git command 454 cmd = ["git"] 455 GitApplyTree.gitCommandUserOptions(cmd, commituser, commitemail) 456 cmd += ["commit", "-F", tmpfile, "--no-verify"] 457 # git doesn't like plain email addresses as authors 458 if author and '<' in author: 459 cmd.append('--author="%s"' % author) 460 if date: 461 cmd.append('--date="%s"' % date) 462 return (tmpfile, cmd) 463 464 @staticmethod 465 def addNote(repo, ref, key, value=None, commituser=None, commitemail=None): 466 note = key + (": %s" % value if value else "") 467 notes_ref = GitApplyTree.notes_ref 468 runcmd(["git", "config", "notes.rewriteMode", "ignore"], repo) 469 runcmd(["git", "config", "notes.displayRef", notes_ref, notes_ref], repo) 470 runcmd(["git", "config", "notes.rewriteRef", notes_ref, notes_ref], repo) 471 cmd = ["git"] 472 GitApplyTree.gitCommandUserOptions(cmd, commituser, commitemail) 473 runcmd(cmd + ["notes", "--ref", notes_ref, "append", "-m", note, ref], repo) 474 475 @staticmethod 476 def removeNote(repo, ref, key, commituser=None, commitemail=None): 477 notes = GitApplyTree.getNotes(repo, ref) 478 notes = {k: v for k, v in notes.items() if k != key and not k.startswith(key + ":")} 479 runcmd(["git", "notes", "--ref", GitApplyTree.notes_ref, "remove", "--ignore-missing", ref], repo) 480 for note, value in notes.items(): 481 GitApplyTree.addNote(repo, ref, note, value, commituser, commitemail) 482 483 @staticmethod 484 def getNotes(repo, ref): 485 import re 486 487 note = None 488 try: 489 note = runcmd(["git", "notes", "--ref", GitApplyTree.notes_ref, "show", ref], repo) 490 prefix = "" 491 except CmdError: 492 note = runcmd(['git', 'show', '-s', '--format=%B', ref], repo) 493 prefix = "%% " 494 495 note_re = re.compile(r'^%s(.*?)(?::\s*(.*))?$' % prefix) 496 notes = dict() 497 for line in note.splitlines(): 498 m = note_re.match(line) 499 if m: 500 notes[m.group(1)] = m.group(2) 501 502 return notes 503 504 @staticmethod 505 def commitIgnored(subject, dir=None, files=None, d=None): 506 if files: 507 runcmd(['git', 'add'] + files, dir) 508 cmd = ["git"] 509 GitApplyTree.gitCommandUserOptions(cmd, d=d) 510 cmd += ["commit", "-m", subject, "--no-verify"] 511 runcmd(cmd, dir) 512 GitApplyTree.addNote(dir, "HEAD", GitApplyTree.ignore_commit, d.getVar('PATCH_GIT_USER_NAME'), d.getVar('PATCH_GIT_USER_EMAIL')) 513 514 @staticmethod 515 def extractPatches(tree, startcommits, outdir, paths=None): 516 import tempfile 517 import shutil 518 tempdir = tempfile.mkdtemp(prefix='oepatch') 519 try: 520 for name, rev in startcommits.items(): 521 shellcmd = ["git", "format-patch", "--no-signature", "--no-numbered", rev, "-o", tempdir] 522 if paths: 523 shellcmd.append('--') 524 shellcmd.extend(paths) 525 out = runcmd(["sh", "-c", " ".join(shellcmd)], os.path.join(tree, name)) 526 if out: 527 for srcfile in out.split(): 528 # This loop, which is used to remove any line that 529 # starts with "%% original patch", is kept for backwards 530 # compatibility. If/when that compatibility is dropped, 531 # it can be replaced with code to just read the first 532 # line of the patch file to get the SHA-1, and the code 533 # below that writes the modified patch file can be 534 # replaced with a simple file move. 535 for encoding in ['utf-8', 'latin-1']: 536 patchlines = [] 537 try: 538 with open(srcfile, 'r', encoding=encoding, newline='') as f: 539 for line in f: 540 if line.startswith("%% " + GitApplyTree.original_patch): 541 continue 542 patchlines.append(line) 543 except UnicodeDecodeError: 544 continue 545 break 546 else: 547 raise PatchError('Unable to find a character encoding to decode %s' % srcfile) 548 549 sha1 = patchlines[0].split()[1] 550 notes = GitApplyTree.getNotes(os.path.join(tree, name), sha1) 551 if GitApplyTree.ignore_commit in notes: 552 continue 553 outfile = notes.get(GitApplyTree.original_patch, os.path.basename(srcfile)) 554 555 bb.utils.mkdirhier(os.path.join(outdir, name)) 556 with open(os.path.join(outdir, name, outfile), 'w') as of: 557 for line in patchlines: 558 of.write(line) 559 finally: 560 shutil.rmtree(tempdir) 561 562 def _need_dirty_check(self): 563 fetch = bb.fetch2.Fetch([], self.d) 564 check_dirtyness = False 565 for url in fetch.urls: 566 url_data = fetch.ud[url] 567 parm = url_data.parm 568 # a git url with subpath param will surely be dirty 569 # since the git tree from which we clone will be emptied 570 # from all files that are not in the subpath 571 if url_data.type == 'git' and parm.get('subpath'): 572 check_dirtyness = True 573 return check_dirtyness 574 575 def _commitpatch(self, patch, patchfilevar): 576 output = "" 577 # Add all files 578 shellcmd = ["git", "add", "-f", "-A", "."] 579 output += runcmd(["sh", "-c", " ".join(shellcmd)], self.dir) 580 # Exclude the patches directory 581 shellcmd = ["git", "reset", "HEAD", self.patchdir] 582 output += runcmd(["sh", "-c", " ".join(shellcmd)], self.dir) 583 # Commit the result 584 (tmpfile, shellcmd) = self.prepareCommit(patch['file'], self.commituser, self.commitemail) 585 try: 586 shellcmd.insert(0, patchfilevar) 587 output += runcmd(["sh", "-c", " ".join(shellcmd)], self.dir) 588 finally: 589 os.remove(tmpfile) 590 return output 591 592 def _applypatch(self, patch, force = False, reverse = False, run = True): 593 import shutil 594 595 def _applypatchhelper(shellcmd, patch, force = False, reverse = False, run = True): 596 if reverse: 597 shellcmd.append('-R') 598 599 shellcmd.append(patch['file']) 600 601 if not run: 602 return "sh" + "-c" + " ".join(shellcmd) 603 604 return runcmd(["sh", "-c", " ".join(shellcmd)], self.dir) 605 606 reporoot = (runcmd("git rev-parse --show-toplevel".split(), self.dir) or '').strip() 607 if not reporoot: 608 raise Exception("Cannot get repository root for directory %s" % self.dir) 609 610 patch_applied = True 611 try: 612 patchfilevar = 'PATCHFILE="%s"' % os.path.basename(patch['file']) 613 if self._need_dirty_check(): 614 # Check dirtyness of the tree 615 try: 616 output = runcmd(["git", "--work-tree=%s" % reporoot, "status", "--short"]) 617 except CmdError: 618 pass 619 else: 620 if output: 621 # The tree is dirty, no need to try to apply patches with git anymore 622 # since they fail, fallback directly to patch 623 output = PatchTree._applypatch(self, patch, force, reverse, run) 624 output += self._commitpatch(patch, patchfilevar) 625 return output 626 try: 627 shellcmd = [patchfilevar, "git", "--work-tree=%s" % reporoot] 628 self.gitCommandUserOptions(shellcmd, self.commituser, self.commitemail) 629 shellcmd += ["am", "-3", "--keep-cr", "--no-scissors", "-p%s" % patch['strippath']] 630 return _applypatchhelper(shellcmd, patch, force, reverse, run) 631 except CmdError: 632 # Need to abort the git am, or we'll still be within it at the end 633 try: 634 shellcmd = ["git", "--work-tree=%s" % reporoot, "am", "--abort"] 635 runcmd(["sh", "-c", " ".join(shellcmd)], self.dir) 636 except CmdError: 637 pass 638 # git am won't always clean up after itself, sadly, so... 639 shellcmd = ["git", "--work-tree=%s" % reporoot, "reset", "--hard", "HEAD"] 640 runcmd(["sh", "-c", " ".join(shellcmd)], self.dir) 641 # Also need to take care of any stray untracked files 642 shellcmd = ["git", "--work-tree=%s" % reporoot, "clean", "-f"] 643 runcmd(["sh", "-c", " ".join(shellcmd)], self.dir) 644 645 # Fall back to git apply 646 shellcmd = ["git", "--git-dir=%s" % reporoot, "apply", "-p%s" % patch['strippath']] 647 try: 648 output = _applypatchhelper(shellcmd, patch, force, reverse, run) 649 except CmdError: 650 # Fall back to patch 651 output = PatchTree._applypatch(self, patch, force, reverse, run) 652 output += self._commitpatch(patch, patchfilevar) 653 return output 654 except: 655 patch_applied = False 656 raise 657 finally: 658 if patch_applied: 659 GitApplyTree.addNote(self.dir, "HEAD", GitApplyTree.original_patch, os.path.basename(patch['file']), self.commituser, self.commitemail) 660 661 662class QuiltTree(PatchSet): 663 def _runcmd(self, args, run = True): 664 quiltrc = self.d.getVar('QUILTRCFILE') 665 if not run: 666 return ["quilt"] + ["--quiltrc"] + [quiltrc] + args 667 runcmd(["quilt"] + ["--quiltrc"] + [quiltrc] + args, self.dir) 668 669 def _quiltpatchpath(self, file): 670 return os.path.join(self.dir, "patches", os.path.basename(file)) 671 672 673 def __init__(self, dir, d): 674 PatchSet.__init__(self, dir, d) 675 self.initialized = False 676 p = os.path.join(self.dir, 'patches') 677 if not os.path.exists(p): 678 os.makedirs(p) 679 680 def Clean(self): 681 try: 682 # make sure that patches/series file exists before quilt pop to keep quilt-0.67 happy 683 open(os.path.join(self.dir, "patches","series"), 'a').close() 684 self._runcmd(["pop", "-a", "-f"]) 685 oe.path.remove(os.path.join(self.dir, "patches","series")) 686 except Exception: 687 pass 688 self.initialized = True 689 690 def InitFromDir(self): 691 # read series -> self.patches 692 seriespath = os.path.join(self.dir, 'patches', 'series') 693 if not os.path.exists(self.dir): 694 raise NotFoundError(self.dir) 695 if os.path.exists(seriespath): 696 with open(seriespath, 'r') as f: 697 for line in f.readlines(): 698 patch = {} 699 parts = line.strip().split() 700 patch["quiltfile"] = self._quiltpatchpath(parts[0]) 701 patch["quiltfilemd5"] = bb.utils.md5_file(patch["quiltfile"]) 702 if len(parts) > 1: 703 patch["strippath"] = parts[1][2:] 704 self.patches.append(patch) 705 706 # determine which patches are applied -> self._current 707 try: 708 output = runcmd(["quilt", "applied"], self.dir) 709 except CmdError: 710 import sys 711 if sys.exc_value.output.strip() == "No patches applied": 712 return 713 else: 714 raise 715 output = [val for val in output.split('\n') if not val.startswith('#')] 716 for patch in self.patches: 717 if os.path.basename(patch["quiltfile"]) == output[-1]: 718 self._current = self.patches.index(patch) 719 self.initialized = True 720 721 def Import(self, patch, force = None): 722 if not self.initialized: 723 self.InitFromDir() 724 PatchSet.Import(self, patch, force) 725 oe.path.symlink(patch["file"], self._quiltpatchpath(patch["file"]), force=True) 726 with open(os.path.join(self.dir, "patches", "series"), "a") as f: 727 f.write(os.path.basename(patch["file"]) + " -p" + patch["strippath"] + "\n") 728 patch["quiltfile"] = self._quiltpatchpath(patch["file"]) 729 patch["quiltfilemd5"] = bb.utils.md5_file(patch["quiltfile"]) 730 731 # TODO: determine if the file being imported: 732 # 1) is already imported, and is the same 733 # 2) is already imported, but differs 734 735 self.patches.insert(self._current or 0, patch) 736 737 738 def Push(self, force = False, all = False, run = True): 739 # quilt push [-f] 740 741 args = ["push"] 742 if force: 743 args.append("-f") 744 if all: 745 args.append("-a") 746 if not run: 747 return self._runcmd(args, run) 748 749 self._runcmd(args) 750 751 if self._current is not None: 752 self._current = self._current + 1 753 else: 754 self._current = 0 755 756 def Pop(self, force = None, all = None): 757 # quilt pop [-f] 758 args = ["pop"] 759 if force: 760 args.append("-f") 761 if all: 762 args.append("-a") 763 764 self._runcmd(args) 765 766 if self._current == 0: 767 self._current = None 768 769 if self._current is not None: 770 self._current = self._current - 1 771 772 def Refresh(self, **kwargs): 773 if kwargs.get("remote"): 774 patch = self.patches[kwargs["patch"]] 775 if not patch: 776 raise PatchError("No patch found at index %s in patchset." % kwargs["patch"]) 777 (type, host, path, user, pswd, parm) = bb.fetch.decodeurl(patch["remote"]) 778 if type == "file": 779 import shutil 780 if not patch.get("file") and patch.get("remote"): 781 patch["file"] = bb.fetch2.localpath(patch["remote"], self.d) 782 783 shutil.copyfile(patch["quiltfile"], patch["file"]) 784 else: 785 raise PatchError("Unable to do a remote refresh of %s, unsupported remote url scheme %s." % (os.path.basename(patch["quiltfile"]), type)) 786 else: 787 # quilt refresh 788 args = ["refresh"] 789 if kwargs.get("quiltfile"): 790 args.append(os.path.basename(kwargs["quiltfile"])) 791 elif kwargs.get("patch"): 792 args.append(os.path.basename(self.patches[kwargs["patch"]]["quiltfile"])) 793 self._runcmd(args) 794 795class Resolver(object): 796 def __init__(self, patchset, terminal): 797 raise NotImplementedError() 798 799 def Resolve(self): 800 raise NotImplementedError() 801 802 def Revert(self): 803 raise NotImplementedError() 804 805 def Finalize(self): 806 raise NotImplementedError() 807 808class NOOPResolver(Resolver): 809 def __init__(self, patchset, terminal): 810 self.patchset = patchset 811 self.terminal = terminal 812 813 def Resolve(self): 814 olddir = os.path.abspath(os.curdir) 815 os.chdir(self.patchset.dir) 816 try: 817 self.patchset.Push() 818 except Exception: 819 import sys 820 raise 821 finally: 822 os.chdir(olddir) 823 824# Patch resolver which relies on the user doing all the work involved in the 825# resolution, with the exception of refreshing the remote copy of the patch 826# files (the urls). 827class UserResolver(Resolver): 828 def __init__(self, patchset, terminal): 829 self.patchset = patchset 830 self.terminal = terminal 831 832 # Force a push in the patchset, then drop to a shell for the user to 833 # resolve any rejected hunks 834 def Resolve(self): 835 olddir = os.path.abspath(os.curdir) 836 os.chdir(self.patchset.dir) 837 try: 838 self.patchset.Push(False) 839 except CmdError as v: 840 # Patch application failed 841 patchcmd = self.patchset.Push(True, False, False) 842 843 t = self.patchset.d.getVar('T') 844 if not t: 845 bb.msg.fatal("Build", "T not set") 846 bb.utils.mkdirhier(t) 847 import random 848 rcfile = "%s/bashrc.%s.%s" % (t, str(os.getpid()), random.random()) 849 with open(rcfile, "w") as f: 850 f.write("echo '*** Manual patch resolution mode ***'\n") 851 f.write("echo 'Dropping to a shell, so patch rejects can be fixed manually.'\n") 852 f.write("echo 'Run \"quilt refresh\" when patch is corrected, press CTRL+D to exit.'\n") 853 f.write("echo ''\n") 854 f.write(" ".join(patchcmd) + "\n") 855 os.chmod(rcfile, 0o775) 856 857 self.terminal("bash --rcfile " + rcfile, 'Patch Rejects: Please fix patch rejects manually', self.patchset.d) 858 859 # Construct a new PatchSet after the user's changes, compare the 860 # sets, checking patches for modifications, and doing a remote 861 # refresh on each. 862 oldpatchset = self.patchset 863 self.patchset = oldpatchset.__class__(self.patchset.dir, self.patchset.d) 864 865 for patch in self.patchset.patches: 866 oldpatch = None 867 for opatch in oldpatchset.patches: 868 if opatch["quiltfile"] == patch["quiltfile"]: 869 oldpatch = opatch 870 871 if oldpatch: 872 patch["remote"] = oldpatch["remote"] 873 if patch["quiltfile"] == oldpatch["quiltfile"]: 874 if patch["quiltfilemd5"] != oldpatch["quiltfilemd5"]: 875 bb.note("Patch %s has changed, updating remote url %s" % (os.path.basename(patch["quiltfile"]), patch["remote"])) 876 # user change? remote refresh 877 self.patchset.Refresh(remote=True, patch=self.patchset.patches.index(patch)) 878 else: 879 # User did not fix the problem. Abort. 880 raise PatchError("Patch application failed, and user did not fix and refresh the patch.") 881 except Exception: 882 raise 883 finally: 884 os.chdir(olddir) 885 886 887def patch_path(url, fetch, unpackdir, expand=True): 888 """Return the local path of a patch, or return nothing if this isn't a patch""" 889 890 local = fetch.localpath(url) 891 if os.path.isdir(local): 892 return 893 base, ext = os.path.splitext(os.path.basename(local)) 894 if ext in ('.gz', '.bz2', '.xz', '.Z'): 895 if expand: 896 local = os.path.join(unpackdir, base) 897 ext = os.path.splitext(base)[1] 898 899 urldata = fetch.ud[url] 900 if "apply" in urldata.parm: 901 apply = oe.types.boolean(urldata.parm["apply"]) 902 if not apply: 903 return 904 elif ext not in (".diff", ".patch"): 905 return 906 907 return local 908 909def src_patches(d, all=False, expand=True): 910 unpackdir = d.getVar('UNPACKDIR') 911 fetch = bb.fetch2.Fetch([], d) 912 patches = [] 913 sources = [] 914 for url in fetch.urls: 915 local = patch_path(url, fetch, unpackdir, expand) 916 if not local: 917 if all: 918 local = fetch.localpath(url) 919 sources.append(local) 920 continue 921 922 urldata = fetch.ud[url] 923 parm = urldata.parm 924 patchname = parm.get('pname') or os.path.basename(local) 925 926 apply, reason = should_apply(parm, d) 927 if not apply: 928 if reason: 929 bb.note("Patch %s %s" % (patchname, reason)) 930 continue 931 932 patchparm = {'patchname': patchname} 933 if "striplevel" in parm: 934 striplevel = parm["striplevel"] 935 elif "pnum" in parm: 936 #bb.msg.warn(None, "Deprecated usage of 'pnum' url parameter in '%s', please use 'striplevel'" % url) 937 striplevel = parm["pnum"] 938 else: 939 striplevel = '1' 940 patchparm['striplevel'] = striplevel 941 942 patchdir = parm.get('patchdir') 943 if patchdir: 944 patchparm['patchdir'] = patchdir 945 946 localurl = bb.fetch.encodeurl(('file', '', local, '', '', patchparm)) 947 patches.append(localurl) 948 949 if all: 950 return sources 951 952 return patches 953 954 955def should_apply(parm, d): 956 import bb.utils 957 if "mindate" in parm or "maxdate" in parm: 958 pn = d.getVar('PN') 959 srcdate = d.getVar('SRCDATE_%s' % pn) 960 if not srcdate: 961 srcdate = d.getVar('SRCDATE') 962 963 if srcdate == "now": 964 srcdate = d.getVar('DATE') 965 966 if "maxdate" in parm and parm["maxdate"] < srcdate: 967 return False, 'is outdated' 968 969 if "mindate" in parm and parm["mindate"] > srcdate: 970 return False, 'is predated' 971 972 973 if "minrev" in parm: 974 srcrev = d.getVar('SRCREV') 975 if srcrev and srcrev < parm["minrev"]: 976 return False, 'applies to later revisions' 977 978 if "maxrev" in parm: 979 srcrev = d.getVar('SRCREV') 980 if srcrev and srcrev > parm["maxrev"]: 981 return False, 'applies to earlier revisions' 982 983 if "rev" in parm: 984 srcrev = d.getVar('SRCREV') 985 if srcrev and parm["rev"] not in srcrev: 986 return False, "doesn't apply to revision" 987 988 if "notrev" in parm: 989 srcrev = d.getVar('SRCREV') 990 if srcrev and parm["notrev"] in srcrev: 991 return False, "doesn't apply to revision" 992 993 if "maxver" in parm: 994 pv = d.getVar('PV') 995 if bb.utils.vercmp_string_op(pv, parm["maxver"], ">"): 996 return False, "applies to earlier version" 997 998 if "minver" in parm: 999 pv = d.getVar('PV') 1000 if bb.utils.vercmp_string_op(pv, parm["minver"], "<"): 1001 return False, "applies to later version" 1002 1003 return True, None 1004