diff options
Diffstat (limited to 'meta/lib/oe/patch.py')
-rw-r--r-- | meta/lib/oe/patch.py | 256 |
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 | ||
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,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 | ||
562 | class QuiltTree(PatchSet): | 660 | class 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 | ||
784 | def patch_path(url, fetch, workdir, expand=True): | 885 | 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""" | 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 | ||
806 | def src_patches(d, all=False, expand=True): | 907 | def 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 | |||