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