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.py256
1 files changed, 178 insertions, 78 deletions
diff --git a/meta/lib/oe/patch.py b/meta/lib/oe/patch.py
index fccbedb519..58c6e34fe8 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,131 @@ 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):
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 runcmd(["git", "notes", "--ref", notes_ref, "append", "-m", note, ref], repo)
472
473 @staticmethod
474 def removeNote(repo, ref, key):
475 notes = GitApplyTree.getNotes(repo, ref)
476 notes = {k: v for k, v in notes.items() if k != key and not k.startswith(key + ":")}
477 runcmd(["git", "notes", "--ref", GitApplyTree.notes_ref, "remove", "--ignore-missing", ref], repo)
478 for note, value in notes.items():
479 GitApplyTree.addNote(repo, ref, note, value)
480
481 @staticmethod
482 def getNotes(repo, ref):
483 import re
484
485 note = None
486 try:
487 note = runcmd(["git", "notes", "--ref", GitApplyTree.notes_ref, "show", ref], repo)
488 prefix = ""
489 except CmdError:
490 note = runcmd(['git', 'show', '-s', '--format=%B', ref], repo)
491 prefix = "%% "
492
493 note_re = re.compile(r'^%s(.*?)(?::\s*(.*))?$' % prefix)
494 notes = dict()
495 for line in note.splitlines():
496 m = note_re.match(line)
497 if m:
498 notes[m.group(1)] = m.group(2)
499
500 return notes
501
502 @staticmethod
503 def commitIgnored(subject, dir=None, files=None, d=None):
504 if files:
505 runcmd(['git', 'add'] + files, dir)
506 cmd = ["git"]
507 GitApplyTree.gitCommandUserOptions(cmd, d=d)
508 cmd += ["commit", "-m", subject, "--no-verify"]
509 runcmd(cmd, dir)
510 GitApplyTree.addNote(dir, "HEAD", GitApplyTree.ignore_commit)
511
512 @staticmethod
513 def extractPatches(tree, startcommits, outdir, paths=None):
444 import tempfile 514 import tempfile
445 import shutil 515 import shutil
446 tempdir = tempfile.mkdtemp(prefix='oepatch') 516 tempdir = tempfile.mkdtemp(prefix='oepatch')
447 try: 517 try:
448 shellcmd = ["git", "format-patch", "--no-signature", "--no-numbered", startcommit, "-o", tempdir] 518 for name, rev in startcommits.items():
449 if paths: 519 shellcmd = ["git", "format-patch", "--no-signature", "--no-numbered", rev, "-o", tempdir]
450 shellcmd.append('--') 520 if paths:
451 shellcmd.extend(paths) 521 shellcmd.append('--')
452 out = runcmd(["sh", "-c", " ".join(shellcmd)], tree) 522 shellcmd.extend(paths)
453 if out: 523 out = runcmd(["sh", "-c", " ".join(shellcmd)], os.path.join(tree, name))
454 for srcfile in out.split(): 524 if out:
455 for encoding in ['utf-8', 'latin-1']: 525 for srcfile in out.split():
456 patchlines = [] 526 # This loop, which is used to remove any line that
457 outfile = None 527 # starts with "%% original patch", is kept for backwards
458 try: 528 # compatibility. If/when that compatibility is dropped,
459 with open(srcfile, 'r', encoding=encoding) as f: 529 # it can be replaced with code to just read the first
460 for line in f: 530 # line of the patch file to get the SHA-1, and the code
461 if line.startswith(GitApplyTree.patch_line_prefix): 531 # below that writes the modified patch file can be
462 outfile = line.split()[-1].strip() 532 # replaced with a simple file move.
463 continue 533 for encoding in ['utf-8', 'latin-1']:
464 if line.startswith(GitApplyTree.ignore_commit_prefix): 534 patchlines = []
465 continue 535 try:
466 patchlines.append(line) 536 with open(srcfile, 'r', encoding=encoding, newline='') as f:
467 except UnicodeDecodeError: 537 for line in f:
538 if line.startswith("%% " + GitApplyTree.original_patch):
539 continue
540 patchlines.append(line)
541 except UnicodeDecodeError:
542 continue
543 break
544 else:
545 raise PatchError('Unable to find a character encoding to decode %s' % srcfile)
546
547 sha1 = patchlines[0].split()[1]
548 notes = GitApplyTree.getNotes(os.path.join(tree, name), sha1)
549 if GitApplyTree.ignore_commit in notes:
468 continue 550 continue
469 break 551 outfile = notes.get(GitApplyTree.original_patch, os.path.basename(srcfile))
470 else: 552
471 raise PatchError('Unable to find a character encoding to decode %s' % srcfile) 553 bb.utils.mkdirhier(os.path.join(outdir, name))
472 554 with open(os.path.join(outdir, name, outfile), 'w') as of:
473 if not outfile: 555 for line in patchlines:
474 outfile = os.path.basename(srcfile) 556 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: 557 finally:
479 shutil.rmtree(tempdir) 558 shutil.rmtree(tempdir)
480 559
560 def _need_dirty_check(self):
561 fetch = bb.fetch2.Fetch([], self.d)
562 check_dirtyness = False
563 for url in fetch.urls:
564 url_data = fetch.ud[url]
565 parm = url_data.parm
566 # a git url with subpath param will surely be dirty
567 # since the git tree from which we clone will be emptied
568 # from all files that are not in the subpath
569 if url_data.type == 'git' and parm.get('subpath'):
570 check_dirtyness = True
571 return check_dirtyness
572
573 def _commitpatch(self, patch, patchfilevar):
574 output = ""
575 # Add all files
576 shellcmd = ["git", "add", "-f", "-A", "."]
577 output += runcmd(["sh", "-c", " ".join(shellcmd)], self.dir)
578 # Exclude the patches directory
579 shellcmd = ["git", "reset", "HEAD", self.patchdir]
580 output += runcmd(["sh", "-c", " ".join(shellcmd)], self.dir)
581 # Commit the result
582 (tmpfile, shellcmd) = self.prepareCommit(patch['file'], self.commituser, self.commitemail)
583 try:
584 shellcmd.insert(0, patchfilevar)
585 output += runcmd(["sh", "-c", " ".join(shellcmd)], self.dir)
586 finally:
587 os.remove(tmpfile)
588 return output
589
481 def _applypatch(self, patch, force = False, reverse = False, run = True): 590 def _applypatch(self, patch, force = False, reverse = False, run = True):
482 import shutil 591 import shutil
483 592
@@ -492,27 +601,26 @@ class GitApplyTree(PatchTree):
492 601
493 return runcmd(["sh", "-c", " ".join(shellcmd)], self.dir) 602 return runcmd(["sh", "-c", " ".join(shellcmd)], self.dir)
494 603
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() 604 reporoot = (runcmd("git rev-parse --show-toplevel".split(), self.dir) or '').strip()
497 if not reporoot: 605 if not reporoot:
498 raise Exception("Cannot get repository root for directory %s" % self.dir) 606 raise Exception("Cannot get repository root for directory %s" % self.dir)
499 hooks_dir = os.path.join(reporoot, '.git', 'hooks') 607
500 hooks_dir_backup = hooks_dir + '.devtool-orig' 608 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: 609 try:
515 patchfilevar = 'PATCHFILE="%s"' % os.path.basename(patch['file']) 610 patchfilevar = 'PATCHFILE="%s"' % os.path.basename(patch['file'])
611 if self._need_dirty_check():
612 # Check dirtyness of the tree
613 try:
614 output = runcmd(["git", "--work-tree=%s" % reporoot, "status", "--short"])
615 except CmdError:
616 pass
617 else:
618 if output:
619 # The tree is dirty, no need to try to apply patches with git anymore
620 # since they fail, fallback directly to patch
621 output = PatchTree._applypatch(self, patch, force, reverse, run)
622 output += self._commitpatch(patch, patchfilevar)
623 return output
516 try: 624 try:
517 shellcmd = [patchfilevar, "git", "--work-tree=%s" % reporoot] 625 shellcmd = [patchfilevar, "git", "--work-tree=%s" % reporoot]
518 self.gitCommandUserOptions(shellcmd, self.commituser, self.commitemail) 626 self.gitCommandUserOptions(shellcmd, self.commituser, self.commitemail)
@@ -539,24 +647,14 @@ class GitApplyTree(PatchTree):
539 except CmdError: 647 except CmdError:
540 # Fall back to patch 648 # Fall back to patch
541 output = PatchTree._applypatch(self, patch, force, reverse, run) 649 output = PatchTree._applypatch(self, patch, force, reverse, run)
542 # Add all files 650 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 651 return output
652 except:
653 patch_applied = False
654 raise
556 finally: 655 finally:
557 shutil.rmtree(hooks_dir) 656 if patch_applied:
558 if os.path.lexists(hooks_dir_backup): 657 GitApplyTree.addNote(self.dir, "HEAD", GitApplyTree.original_patch, os.path.basename(patch['file']))
559 shutil.move(hooks_dir_backup, hooks_dir)
560 658
561 659
562class QuiltTree(PatchSet): 660class QuiltTree(PatchSet):
@@ -579,6 +677,8 @@ class QuiltTree(PatchSet):
579 677
580 def Clean(self): 678 def Clean(self):
581 try: 679 try:
680 # make sure that patches/series file exists before quilt pop to keep quilt-0.67 happy
681 open(os.path.join(self.dir, "patches","series"), 'a').close()
582 self._runcmd(["pop", "-a", "-f"]) 682 self._runcmd(["pop", "-a", "-f"])
583 oe.path.remove(os.path.join(self.dir, "patches","series")) 683 oe.path.remove(os.path.join(self.dir, "patches","series"))
584 except Exception: 684 except Exception:
@@ -715,8 +815,9 @@ class NOOPResolver(Resolver):
715 self.patchset.Push() 815 self.patchset.Push()
716 except Exception: 816 except Exception:
717 import sys 817 import sys
718 os.chdir(olddir)
719 raise 818 raise
819 finally:
820 os.chdir(olddir)
720 821
721# Patch resolver which relies on the user doing all the work involved in the 822# 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 823# resolution, with the exception of refreshing the remote copy of the patch
@@ -776,12 +877,12 @@ class UserResolver(Resolver):
776 # User did not fix the problem. Abort. 877 # User did not fix the problem. Abort.
777 raise PatchError("Patch application failed, and user did not fix and refresh the patch.") 878 raise PatchError("Patch application failed, and user did not fix and refresh the patch.")
778 except Exception: 879 except Exception:
779 os.chdir(olddir)
780 raise 880 raise
781 os.chdir(olddir) 881 finally:
882 os.chdir(olddir)
782 883
783 884
784def patch_path(url, fetch, workdir, expand=True): 885def patch_path(url, fetch, unpackdir, expand=True):
785 """Return the local path of a patch, or return nothing if this isn't a patch""" 886 """Return the local path of a patch, or return nothing if this isn't a patch"""
786 887
787 local = fetch.localpath(url) 888 local = fetch.localpath(url)
@@ -790,7 +891,7 @@ def patch_path(url, fetch, workdir, expand=True):
790 base, ext = os.path.splitext(os.path.basename(local)) 891 base, ext = os.path.splitext(os.path.basename(local))
791 if ext in ('.gz', '.bz2', '.xz', '.Z'): 892 if ext in ('.gz', '.bz2', '.xz', '.Z'):
792 if expand: 893 if expand:
793 local = os.path.join(workdir, base) 894 local = os.path.join(unpackdir, base)
794 ext = os.path.splitext(base)[1] 895 ext = os.path.splitext(base)[1]
795 896
796 urldata = fetch.ud[url] 897 urldata = fetch.ud[url]
@@ -804,12 +905,12 @@ def patch_path(url, fetch, workdir, expand=True):
804 return local 905 return local
805 906
806def src_patches(d, all=False, expand=True): 907def src_patches(d, all=False, expand=True):
807 workdir = d.getVar('WORKDIR') 908 unpackdir = d.getVar('UNPACKDIR')
808 fetch = bb.fetch2.Fetch([], d) 909 fetch = bb.fetch2.Fetch([], d)
809 patches = [] 910 patches = []
810 sources = [] 911 sources = []
811 for url in fetch.urls: 912 for url in fetch.urls:
812 local = patch_path(url, fetch, workdir, expand) 913 local = patch_path(url, fetch, unpackdir, expand)
813 if not local: 914 if not local:
814 if all: 915 if all:
815 local = fetch.localpath(url) 916 local = fetch.localpath(url)
@@ -898,4 +999,3 @@ def should_apply(parm, d):
898 return False, "applies to later version" 999 return False, "applies to later version"
899 1000
900 return True, None 1001 return True, None
901