diff options
Diffstat (limited to 'meta/lib/oe/patch.py')
-rw-r--r-- | meta/lib/oe/patch.py | 258 |
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 | ||
7 | import os | ||
8 | import shlex | ||
9 | import subprocess | ||
5 | import oe.path | 10 | import oe.path |
6 | import oe.types | 11 | import oe.types |
7 | 12 | ||
@@ -24,9 +29,6 @@ class CmdError(bb.BBHandledException): | |||
24 | 29 | ||
25 | 30 | ||
26 | def runcmd(args, dir = None): | 31 | def 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 | |||
59 | class PatchError(Exception): | 62 | class 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 | ||
293 | class GitApplyTree(PatchTree): | 296 | class 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 | ||
562 | class QuiltTree(PatchSet): | 662 | class 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 | ||
784 | def patch_path(url, fetch, workdir, expand=True): | 887 | def 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 | ||
806 | def src_patches(d, all=False, expand=True): | 909 | def 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 | |||