summaryrefslogtreecommitdiffstats
path: root/meta/lib/oe/patch.py
diff options
context:
space:
mode:
Diffstat (limited to 'meta/lib/oe/patch.py')
-rw-r--r--meta/lib/oe/patch.py258
1 files changed, 180 insertions, 78 deletions
diff --git a/meta/lib/oe/patch.py b/meta/lib/oe/patch.py
index fccbedb519..edd77196ee 100644
--- a/meta/lib/oe/patch.py
+++ b/meta/lib/oe/patch.py
@@ -1,7 +1,12 @@
1# 1#
2# Copyright OpenEmbedded Contributors
3#
2# SPDX-License-Identifier: GPL-2.0-only 4# SPDX-License-Identifier: GPL-2.0-only
3# 5#
4 6
7import os
8import shlex
9import subprocess
5import oe.path 10import oe.path
6import oe.types 11import oe.types
7 12
@@ -24,9 +29,6 @@ class CmdError(bb.BBHandledException):
24 29
25 30
26def runcmd(args, dir = None): 31def runcmd(args, dir = None):
27 import pipes
28 import subprocess
29
30 if dir: 32 if dir:
31 olddir = os.path.abspath(os.curdir) 33 olddir = os.path.abspath(os.curdir)
32 if not os.path.exists(dir): 34 if not os.path.exists(dir):
@@ -35,7 +37,7 @@ def runcmd(args, dir = None):
35 # print("cwd: %s -> %s" % (olddir, dir)) 37 # print("cwd: %s -> %s" % (olddir, dir))
36 38
37 try: 39 try:
38 args = [ pipes.quote(str(arg)) for arg in args ] 40 args = [ shlex.quote(str(arg)) for arg in args ]
39 cmd = " ".join(args) 41 cmd = " ".join(args)
40 # print("cmd: %s" % cmd) 42 # print("cmd: %s" % cmd)
41 proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True) 43 proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
@@ -56,6 +58,7 @@ def runcmd(args, dir = None):
56 if dir: 58 if dir:
57 os.chdir(olddir) 59 os.chdir(olddir)
58 60
61
59class PatchError(Exception): 62class PatchError(Exception):
60 def __init__(self, msg): 63 def __init__(self, msg):
61 self.msg = msg 64 self.msg = msg
@@ -214,7 +217,7 @@ class PatchTree(PatchSet):
214 with open(self.seriespath, 'w') as f: 217 with open(self.seriespath, 'w') as f:
215 for p in patches: 218 for p in patches:
216 f.write(p) 219 f.write(p)
217 220
218 def Import(self, patch, force = None): 221 def Import(self, patch, force = None):
219 """""" 222 """"""
220 PatchSet.Import(self, patch, force) 223 PatchSet.Import(self, patch, force)
@@ -291,13 +294,32 @@ class PatchTree(PatchSet):
291 self.Pop(all=True) 294 self.Pop(all=True)
292 295
293class GitApplyTree(PatchTree): 296class GitApplyTree(PatchTree):
294 patch_line_prefix = '%% original patch' 297 notes_ref = "refs/notes/devtool"
295 ignore_commit_prefix = '%% ignore' 298 original_patch = 'original patch'
299 ignore_commit = 'ignore'
296 300
297 def __init__(self, dir, d): 301 def __init__(self, dir, d):
298 PatchTree.__init__(self, dir, d) 302 PatchTree.__init__(self, dir, d)
299 self.commituser = d.getVar('PATCH_GIT_USER_NAME') 303 self.commituser = d.getVar('PATCH_GIT_USER_NAME')
300 self.commitemail = d.getVar('PATCH_GIT_USER_EMAIL') 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)
301 323
302 @staticmethod 324 @staticmethod
303 def extractPatchHeader(patchfile): 325 def extractPatchHeader(patchfile):
@@ -431,7 +453,7 @@ class GitApplyTree(PatchTree):
431 # Prepare git command 453 # Prepare git command
432 cmd = ["git"] 454 cmd = ["git"]
433 GitApplyTree.gitCommandUserOptions(cmd, commituser, commitemail) 455 GitApplyTree.gitCommandUserOptions(cmd, commituser, commitemail)
434 cmd += ["commit", "-F", tmpfile] 456 cmd += ["commit", "-F", tmpfile, "--no-verify"]
435 # git doesn't like plain email addresses as authors 457 # git doesn't like plain email addresses as authors
436 if author and '<' in author: 458 if author and '<' in author:
437 cmd.append('--author="%s"' % author) 459 cmd.append('--author="%s"' % author)
@@ -440,44 +462,133 @@ class GitApplyTree(PatchTree):
440 return (tmpfile, cmd) 462 return (tmpfile, cmd)
441 463
442 @staticmethod 464 @staticmethod
443 def extractPatches(tree, startcommit, outdir, paths=None): 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):
444 import tempfile 516 import tempfile
445 import shutil 517 import shutil
446 tempdir = tempfile.mkdtemp(prefix='oepatch') 518 tempdir = tempfile.mkdtemp(prefix='oepatch')
447 try: 519 try:
448 shellcmd = ["git", "format-patch", "--no-signature", "--no-numbered", startcommit, "-o", tempdir] 520 for name, rev in startcommits.items():
449 if paths: 521 shellcmd = ["git", "format-patch", "--no-signature", "--no-numbered", rev, "-o", tempdir]
450 shellcmd.append('--') 522 if paths:
451 shellcmd.extend(paths) 523 shellcmd.append('--')
452 out = runcmd(["sh", "-c", " ".join(shellcmd)], tree) 524 shellcmd.extend(paths)
453 if out: 525 out = runcmd(["sh", "-c", " ".join(shellcmd)], os.path.join(tree, name))
454 for srcfile in out.split(): 526 if out:
455 for encoding in ['utf-8', 'latin-1']: 527 for srcfile in out.split():
456 patchlines = [] 528 # This loop, which is used to remove any line that
457 outfile = None 529 # starts with "%% original patch", is kept for backwards
458 try: 530 # compatibility. If/when that compatibility is dropped,
459 with open(srcfile, 'r', encoding=encoding) as f: 531 # it can be replaced with code to just read the first
460 for line in f: 532 # line of the patch file to get the SHA-1, and the code
461 if line.startswith(GitApplyTree.patch_line_prefix): 533 # below that writes the modified patch file can be
462 outfile = line.split()[-1].strip() 534 # replaced with a simple file move.
463 continue 535 for encoding in ['utf-8', 'latin-1']:
464 if line.startswith(GitApplyTree.ignore_commit_prefix): 536 patchlines = []
465 continue 537 try:
466 patchlines.append(line) 538 with open(srcfile, 'r', encoding=encoding, newline='') as f:
467 except UnicodeDecodeError: 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:
468 continue 552 continue
469 break 553 outfile = notes.get(GitApplyTree.original_patch, os.path.basename(srcfile))
470 else: 554
471 raise PatchError('Unable to find a character encoding to decode %s' % srcfile) 555 bb.utils.mkdirhier(os.path.join(outdir, name))
472 556 with open(os.path.join(outdir, name, outfile), 'w') as of:
473 if not outfile: 557 for line in patchlines:
474 outfile = os.path.basename(srcfile) 558 of.write(line)
475 with open(os.path.join(outdir, outfile), 'w') as of:
476 for line in patchlines:
477 of.write(line)
478 finally: 559 finally:
479 shutil.rmtree(tempdir) 560 shutil.rmtree(tempdir)
480 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
481 def _applypatch(self, patch, force = False, reverse = False, run = True): 592 def _applypatch(self, patch, force = False, reverse = False, run = True):
482 import shutil 593 import shutil
483 594
@@ -492,27 +603,26 @@ class GitApplyTree(PatchTree):
492 603
493 return runcmd(["sh", "-c", " ".join(shellcmd)], self.dir) 604 return runcmd(["sh", "-c", " ".join(shellcmd)], self.dir)
494 605
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() 606 reporoot = (runcmd("git rev-parse --show-toplevel".split(), self.dir) or '').strip()
497 if not reporoot: 607 if not reporoot:
498 raise Exception("Cannot get repository root for directory %s" % self.dir) 608 raise Exception("Cannot get repository root for directory %s" % self.dir)
499 hooks_dir = os.path.join(reporoot, '.git', 'hooks') 609
500 hooks_dir_backup = hooks_dir + '.devtool-orig' 610 patch_applied = True
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: 611 try:
515 patchfilevar = 'PATCHFILE="%s"' % os.path.basename(patch['file']) 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
516 try: 626 try:
517 shellcmd = [patchfilevar, "git", "--work-tree=%s" % reporoot] 627 shellcmd = [patchfilevar, "git", "--work-tree=%s" % reporoot]
518 self.gitCommandUserOptions(shellcmd, self.commituser, self.commitemail) 628 self.gitCommandUserOptions(shellcmd, self.commituser, self.commitemail)
@@ -539,24 +649,14 @@ class GitApplyTree(PatchTree):
539 except CmdError: 649 except CmdError:
540 # Fall back to patch 650 # Fall back to patch
541 output = PatchTree._applypatch(self, patch, force, reverse, run) 651 output = PatchTree._applypatch(self, patch, force, reverse, run)
542 # Add all files 652 output += self._commitpatch(patch, patchfilevar)
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 653 return output
654 except:
655 patch_applied = False
656 raise
556 finally: 657 finally:
557 shutil.rmtree(hooks_dir) 658 if patch_applied:
558 if os.path.lexists(hooks_dir_backup): 659 GitApplyTree.addNote(self.dir, "HEAD", GitApplyTree.original_patch, os.path.basename(patch['file']), self.commituser, self.commitemail)
559 shutil.move(hooks_dir_backup, hooks_dir)
560 660
561 661
562class QuiltTree(PatchSet): 662class QuiltTree(PatchSet):
@@ -579,6 +679,8 @@ class QuiltTree(PatchSet):
579 679
580 def Clean(self): 680 def Clean(self):
581 try: 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()
582 self._runcmd(["pop", "-a", "-f"]) 684 self._runcmd(["pop", "-a", "-f"])
583 oe.path.remove(os.path.join(self.dir, "patches","series")) 685 oe.path.remove(os.path.join(self.dir, "patches","series"))
584 except Exception: 686 except Exception:
@@ -715,8 +817,9 @@ class NOOPResolver(Resolver):
715 self.patchset.Push() 817 self.patchset.Push()
716 except Exception: 818 except Exception:
717 import sys 819 import sys
718 os.chdir(olddir)
719 raise 820 raise
821 finally:
822 os.chdir(olddir)
720 823
721# Patch resolver which relies on the user doing all the work involved in the 824# 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 825# resolution, with the exception of refreshing the remote copy of the patch
@@ -776,12 +879,12 @@ class UserResolver(Resolver):
776 # User did not fix the problem. Abort. 879 # User did not fix the problem. Abort.
777 raise PatchError("Patch application failed, and user did not fix and refresh the patch.") 880 raise PatchError("Patch application failed, and user did not fix and refresh the patch.")
778 except Exception: 881 except Exception:
779 os.chdir(olddir)
780 raise 882 raise
781 os.chdir(olddir) 883 finally:
884 os.chdir(olddir)
782 885
783 886
784def patch_path(url, fetch, workdir, expand=True): 887def patch_path(url, fetch, unpackdir, expand=True):
785 """Return the local path of a patch, or return nothing if this isn't a patch""" 888 """Return the local path of a patch, or return nothing if this isn't a patch"""
786 889
787 local = fetch.localpath(url) 890 local = fetch.localpath(url)
@@ -790,7 +893,7 @@ def patch_path(url, fetch, workdir, expand=True):
790 base, ext = os.path.splitext(os.path.basename(local)) 893 base, ext = os.path.splitext(os.path.basename(local))
791 if ext in ('.gz', '.bz2', '.xz', '.Z'): 894 if ext in ('.gz', '.bz2', '.xz', '.Z'):
792 if expand: 895 if expand:
793 local = os.path.join(workdir, base) 896 local = os.path.join(unpackdir, base)
794 ext = os.path.splitext(base)[1] 897 ext = os.path.splitext(base)[1]
795 898
796 urldata = fetch.ud[url] 899 urldata = fetch.ud[url]
@@ -804,12 +907,12 @@ def patch_path(url, fetch, workdir, expand=True):
804 return local 907 return local
805 908
806def src_patches(d, all=False, expand=True): 909def src_patches(d, all=False, expand=True):
807 workdir = d.getVar('WORKDIR') 910 unpackdir = d.getVar('UNPACKDIR')
808 fetch = bb.fetch2.Fetch([], d) 911 fetch = bb.fetch2.Fetch([], d)
809 patches = [] 912 patches = []
810 sources = [] 913 sources = []
811 for url in fetch.urls: 914 for url in fetch.urls:
812 local = patch_path(url, fetch, workdir, expand) 915 local = patch_path(url, fetch, unpackdir, expand)
813 if not local: 916 if not local:
814 if all: 917 if all:
815 local = fetch.localpath(url) 918 local = fetch.localpath(url)
@@ -898,4 +1001,3 @@ def should_apply(parm, d):
898 return False, "applies to later version" 1001 return False, "applies to later version"
899 1002
900 return True, None 1003 return True, None
901