| # |
| # Copyright OpenEmbedded Contributors |
| # |
| # SPDX-License-Identifier: GPL-2.0-only |
| # |
| |
| import os |
| import shlex |
| import subprocess |
| import oe.path |
| import oe.types |
| |
| class NotFoundError(bb.BBHandledException): |
| def __init__(self, path): |
| self.path = path |
| |
| def __str__(self): |
| return "Error: %s not found." % self.path |
| |
| class CmdError(bb.BBHandledException): |
| def __init__(self, command, exitstatus, output): |
| self.command = command |
| self.status = exitstatus |
| self.output = output |
| |
| def __str__(self): |
| return "Command Error: '%s' exited with %d Output:\n%s" % \ |
| (self.command, self.status, self.output) |
| |
| |
| def runcmd(args, dir = None): |
| if dir: |
| olddir = os.path.abspath(os.curdir) |
| if not os.path.exists(dir): |
| raise NotFoundError(dir) |
| os.chdir(dir) |
| # print("cwd: %s -> %s" % (olddir, dir)) |
| |
| try: |
| args = [ shlex.quote(str(arg)) for arg in args ] |
| cmd = " ".join(args) |
| # print("cmd: %s" % cmd) |
| proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True) |
| stdout, stderr = proc.communicate() |
| stdout = stdout.decode('utf-8') |
| stderr = stderr.decode('utf-8') |
| exitstatus = proc.returncode |
| if exitstatus != 0: |
| raise CmdError(cmd, exitstatus >> 8, "stdout: %s\nstderr: %s" % (stdout, stderr)) |
| if " fuzz " in stdout and "Hunk " in stdout: |
| # Drop patch fuzz info with header and footer to log file so |
| # insane.bbclass can handle to throw error/warning |
| bb.note("--- Patch fuzz start ---\n%s\n--- Patch fuzz end ---" % format(stdout)) |
| |
| return stdout |
| |
| finally: |
| if dir: |
| os.chdir(olddir) |
| |
| |
| class PatchError(Exception): |
| def __init__(self, msg): |
| self.msg = msg |
| |
| def __str__(self): |
| return "Patch Error: %s" % self.msg |
| |
| class PatchSet(object): |
| defaults = { |
| "strippath": 1 |
| } |
| |
| def __init__(self, dir, d): |
| self.dir = dir |
| self.d = d |
| self.patches = [] |
| self._current = None |
| |
| def current(self): |
| return self._current |
| |
| def Clean(self): |
| """ |
| Clean out the patch set. Generally includes unapplying all |
| patches and wiping out all associated metadata. |
| """ |
| raise NotImplementedError() |
| |
| def Import(self, patch, force): |
| if not patch.get("file"): |
| if not patch.get("remote"): |
| raise PatchError("Patch file must be specified in patch import.") |
| else: |
| patch["file"] = bb.fetch2.localpath(patch["remote"], self.d) |
| |
| for param in PatchSet.defaults: |
| if not patch.get(param): |
| patch[param] = PatchSet.defaults[param] |
| |
| if patch.get("remote"): |
| patch["file"] = self.d.expand(bb.fetch2.localpath(patch["remote"], self.d)) |
| |
| patch["filemd5"] = bb.utils.md5_file(patch["file"]) |
| |
| def Push(self, force): |
| raise NotImplementedError() |
| |
| def Pop(self, force): |
| raise NotImplementedError() |
| |
| def Refresh(self, remote = None, all = None): |
| raise NotImplementedError() |
| |
| @staticmethod |
| def getPatchedFiles(patchfile, striplevel, srcdir=None): |
| """ |
| Read a patch file and determine which files it will modify. |
| Params: |
| patchfile: the patch file to read |
| striplevel: the strip level at which the patch is going to be applied |
| srcdir: optional path to join onto the patched file paths |
| Returns: |
| A list of tuples of file path and change mode ('A' for add, |
| 'D' for delete or 'M' for modify) |
| """ |
| |
| def patchedpath(patchline): |
| filepth = patchline.split()[1] |
| if filepth.endswith('/dev/null'): |
| return '/dev/null' |
| filesplit = filepth.split(os.sep) |
| if striplevel > len(filesplit): |
| bb.error('Patch %s has invalid strip level %d' % (patchfile, striplevel)) |
| return None |
| return os.sep.join(filesplit[striplevel:]) |
| |
| for encoding in ['utf-8', 'latin-1']: |
| try: |
| copiedmode = False |
| filelist = [] |
| with open(patchfile) as f: |
| for line in f: |
| if line.startswith('--- '): |
| patchpth = patchedpath(line) |
| if not patchpth: |
| break |
| if copiedmode: |
| addedfile = patchpth |
| else: |
| removedfile = patchpth |
| elif line.startswith('+++ '): |
| addedfile = patchedpath(line) |
| if not addedfile: |
| break |
| elif line.startswith('*** '): |
| copiedmode = True |
| removedfile = patchedpath(line) |
| if not removedfile: |
| break |
| else: |
| removedfile = None |
| addedfile = None |
| |
| if addedfile and removedfile: |
| if removedfile == '/dev/null': |
| mode = 'A' |
| elif addedfile == '/dev/null': |
| mode = 'D' |
| else: |
| mode = 'M' |
| if srcdir: |
| fullpath = os.path.abspath(os.path.join(srcdir, addedfile)) |
| else: |
| fullpath = addedfile |
| filelist.append((fullpath, mode)) |
| except UnicodeDecodeError: |
| continue |
| break |
| else: |
| raise PatchError('Unable to decode %s' % patchfile) |
| |
| return filelist |
| |
| |
| class PatchTree(PatchSet): |
| def __init__(self, dir, d): |
| PatchSet.__init__(self, dir, d) |
| self.patchdir = os.path.join(self.dir, 'patches') |
| self.seriespath = os.path.join(self.dir, 'patches', 'series') |
| bb.utils.mkdirhier(self.patchdir) |
| |
| def _appendPatchFile(self, patch, strippath): |
| with open(self.seriespath, 'a') as f: |
| f.write(os.path.basename(patch) + "," + strippath + "\n") |
| shellcmd = ["cat", patch, ">" , self.patchdir + "/" + os.path.basename(patch)] |
| runcmd(["sh", "-c", " ".join(shellcmd)], self.dir) |
| |
| def _removePatch(self, p): |
| patch = {} |
| patch['file'] = p.split(",")[0] |
| patch['strippath'] = p.split(",")[1] |
| self._applypatch(patch, False, True) |
| |
| def _removePatchFile(self, all = False): |
| if not os.path.exists(self.seriespath): |
| return |
| with open(self.seriespath, 'r+') as f: |
| patches = f.readlines() |
| if all: |
| for p in reversed(patches): |
| self._removePatch(os.path.join(self.patchdir, p.strip())) |
| patches = [] |
| else: |
| self._removePatch(os.path.join(self.patchdir, patches[-1].strip())) |
| patches.pop() |
| with open(self.seriespath, 'w') as f: |
| for p in patches: |
| f.write(p) |
| |
| def Import(self, patch, force = None): |
| """""" |
| PatchSet.Import(self, patch, force) |
| |
| if self._current is not None: |
| i = self._current + 1 |
| else: |
| i = 0 |
| self.patches.insert(i, patch) |
| |
| def _applypatch(self, patch, force = False, reverse = False, run = True): |
| shellcmd = ["cat", patch['file'], "|", "patch", "--no-backup-if-mismatch", "-p", patch['strippath']] |
| if reverse: |
| shellcmd.append('-R') |
| |
| if not run: |
| return "sh" + "-c" + " ".join(shellcmd) |
| |
| if not force: |
| shellcmd.append('--dry-run') |
| |
| try: |
| output = runcmd(["sh", "-c", " ".join(shellcmd)], self.dir) |
| |
| if force: |
| return |
| |
| shellcmd.pop(len(shellcmd) - 1) |
| output = runcmd(["sh", "-c", " ".join(shellcmd)], self.dir) |
| except CmdError as err: |
| raise bb.BBHandledException("Applying '%s' failed:\n%s" % |
| (os.path.basename(patch['file']), err.output)) |
| |
| if not reverse: |
| self._appendPatchFile(patch['file'], patch['strippath']) |
| |
| return output |
| |
| def Push(self, force = False, all = False, run = True): |
| bb.note("self._current is %s" % self._current) |
| bb.note("patches is %s" % self.patches) |
| if all: |
| for i in self.patches: |
| bb.note("applying patch %s" % i) |
| self._applypatch(i, force) |
| self._current = i |
| else: |
| if self._current is not None: |
| next = self._current + 1 |
| else: |
| next = 0 |
| |
| bb.note("applying patch %s" % self.patches[next]) |
| ret = self._applypatch(self.patches[next], force) |
| |
| self._current = next |
| return ret |
| |
| def Pop(self, force = None, all = None): |
| if all: |
| self._removePatchFile(True) |
| self._current = None |
| else: |
| self._removePatchFile(False) |
| |
| if self._current == 0: |
| self._current = None |
| |
| if self._current is not None: |
| self._current = self._current - 1 |
| |
| def Clean(self): |
| """""" |
| self.Pop(all=True) |
| |
| class GitApplyTree(PatchTree): |
| notes_ref = "refs/notes/devtool" |
| original_patch = 'original patch' |
| ignore_commit = 'ignore' |
| |
| def __init__(self, dir, d): |
| PatchTree.__init__(self, dir, d) |
| self.commituser = d.getVar('PATCH_GIT_USER_NAME') |
| self.commitemail = d.getVar('PATCH_GIT_USER_EMAIL') |
| if not self._isInitialized(d): |
| self._initRepo() |
| |
| def _isInitialized(self, d): |
| cmd = "git rev-parse --show-toplevel" |
| try: |
| output = runcmd(cmd.split(), self.dir).strip() |
| except CmdError as err: |
| ## runcmd returned non-zero which most likely means 128 |
| ## Not a git directory |
| return False |
| ## Make sure repo is in builddir to not break top-level git repos, or under workdir |
| return os.path.samefile(output, self.dir) or oe.path.is_path_parent(d.getVar('WORKDIR'), output) |
| |
| def _initRepo(self): |
| runcmd("git init".split(), self.dir) |
| runcmd("git add .".split(), self.dir) |
| runcmd("git commit -a --allow-empty -m bitbake_patching_started".split(), self.dir) |
| |
| @staticmethod |
| def extractPatchHeader(patchfile): |
| """ |
| Extract just the header lines from the top of a patch file |
| """ |
| for encoding in ['utf-8', 'latin-1']: |
| lines = [] |
| try: |
| with open(patchfile, 'r', encoding=encoding) as f: |
| for line in f: |
| if line.startswith('Index: ') or line.startswith('diff -') or line.startswith('---'): |
| break |
| lines.append(line) |
| except UnicodeDecodeError: |
| continue |
| break |
| else: |
| raise PatchError('Unable to find a character encoding to decode %s' % patchfile) |
| return lines |
| |
| @staticmethod |
| def decodeAuthor(line): |
| from email.header import decode_header |
| authorval = line.split(':', 1)[1].strip().replace('"', '') |
| result = decode_header(authorval)[0][0] |
| if hasattr(result, 'decode'): |
| result = result.decode('utf-8') |
| return result |
| |
| @staticmethod |
| def interpretPatchHeader(headerlines): |
| import re |
| author_re = re.compile(r'[\S ]+ <\S+@\S+\.\S+>') |
| from_commit_re = re.compile(r'^From [a-z0-9]{40} .*') |
| outlines = [] |
| author = None |
| date = None |
| subject = None |
| for line in headerlines: |
| if line.startswith('Subject: '): |
| subject = line.split(':', 1)[1] |
| # Remove any [PATCH][oe-core] etc. |
| subject = re.sub(r'\[.+?\]\s*', '', subject) |
| continue |
| elif line.startswith('From: ') or line.startswith('Author: '): |
| authorval = GitApplyTree.decodeAuthor(line) |
| # git is fussy about author formatting i.e. it must be Name <email@domain> |
| if author_re.match(authorval): |
| author = authorval |
| continue |
| elif line.startswith('Date: '): |
| if date is None: |
| dateval = line.split(':', 1)[1].strip() |
| # Very crude check for date format, since git will blow up if it's not in the right |
| # format. Without e.g. a python-dateutils dependency we can't do a whole lot more |
| if len(dateval) > 12: |
| date = dateval |
| continue |
| elif not author and line.lower().startswith('signed-off-by: '): |
| authorval = GitApplyTree.decodeAuthor(line) |
| # git is fussy about author formatting i.e. it must be Name <email@domain> |
| if author_re.match(authorval): |
| author = authorval |
| elif from_commit_re.match(line): |
| # We don't want the From <commit> line - if it's present it will break rebasing |
| continue |
| outlines.append(line) |
| |
| if not subject: |
| firstline = None |
| for line in headerlines: |
| line = line.strip() |
| if firstline: |
| if line: |
| # Second line is not blank, the first line probably isn't usable |
| firstline = None |
| break |
| elif line: |
| firstline = line |
| if firstline and not firstline.startswith(('#', 'Index:', 'Upstream-Status:')) and len(firstline) < 100: |
| subject = firstline |
| |
| return outlines, author, date, subject |
| |
| @staticmethod |
| def gitCommandUserOptions(cmd, commituser=None, commitemail=None, d=None): |
| if d: |
| commituser = d.getVar('PATCH_GIT_USER_NAME') |
| commitemail = d.getVar('PATCH_GIT_USER_EMAIL') |
| if commituser: |
| cmd += ['-c', 'user.name="%s"' % commituser] |
| if commitemail: |
| cmd += ['-c', 'user.email="%s"' % commitemail] |
| |
| @staticmethod |
| def prepareCommit(patchfile, commituser=None, commitemail=None): |
| """ |
| Prepare a git commit command line based on the header from a patch file |
| (typically this is useful for patches that cannot be applied with "git am" due to formatting) |
| """ |
| import tempfile |
| # Process patch header and extract useful information |
| lines = GitApplyTree.extractPatchHeader(patchfile) |
| outlines, author, date, subject = GitApplyTree.interpretPatchHeader(lines) |
| if not author or not subject or not date: |
| try: |
| shellcmd = ["git", "log", "--format=email", "--follow", "--diff-filter=A", "--", patchfile] |
| out = runcmd(["sh", "-c", " ".join(shellcmd)], os.path.dirname(patchfile)) |
| except CmdError: |
| out = None |
| if out: |
| _, newauthor, newdate, newsubject = GitApplyTree.interpretPatchHeader(out.splitlines()) |
| if not author: |
| # If we're setting the author then the date should be set as well |
| author = newauthor |
| date = newdate |
| elif not date: |
| # If we don't do this we'll get the current date, at least this will be closer |
| date = newdate |
| if not subject: |
| subject = newsubject |
| if subject and not (outlines and outlines[0].strip() == subject): |
| outlines.insert(0, '%s\n\n' % subject.strip()) |
| |
| # Write out commit message to a file |
| with tempfile.NamedTemporaryFile('w', delete=False) as tf: |
| tmpfile = tf.name |
| for line in outlines: |
| tf.write(line) |
| # Prepare git command |
| cmd = ["git"] |
| GitApplyTree.gitCommandUserOptions(cmd, commituser, commitemail) |
| cmd += ["commit", "-F", tmpfile, "--no-verify"] |
| # git doesn't like plain email addresses as authors |
| if author and '<' in author: |
| cmd.append('--author="%s"' % author) |
| if date: |
| cmd.append('--date="%s"' % date) |
| return (tmpfile, cmd) |
| |
| @staticmethod |
| def addNote(repo, ref, key, value=None): |
| note = key + (": %s" % value if value else "") |
| notes_ref = GitApplyTree.notes_ref |
| runcmd(["git", "config", "notes.rewriteMode", "ignore"], repo) |
| runcmd(["git", "config", "notes.displayRef", notes_ref, notes_ref], repo) |
| runcmd(["git", "config", "notes.rewriteRef", notes_ref, notes_ref], repo) |
| runcmd(["git", "notes", "--ref", notes_ref, "append", "-m", note, ref], repo) |
| |
| @staticmethod |
| def removeNote(repo, ref, key): |
| notes = GitApplyTree.getNotes(repo, ref) |
| notes = {k: v for k, v in notes.items() if k != key and not k.startswith(key + ":")} |
| runcmd(["git", "notes", "--ref", GitApplyTree.notes_ref, "remove", "--ignore-missing", ref], repo) |
| for note, value in notes.items(): |
| GitApplyTree.addNote(repo, ref, note, value) |
| |
| @staticmethod |
| def getNotes(repo, ref): |
| import re |
| |
| note = None |
| try: |
| note = runcmd(["git", "notes", "--ref", GitApplyTree.notes_ref, "show", ref], repo) |
| prefix = "" |
| except CmdError: |
| note = runcmd(['git', 'show', '-s', '--format=%B', ref], repo) |
| prefix = "%% " |
| |
| note_re = re.compile(r'^%s(.*?)(?::\s*(.*))?$' % prefix) |
| notes = dict() |
| for line in note.splitlines(): |
| m = note_re.match(line) |
| if m: |
| notes[m.group(1)] = m.group(2) |
| |
| return notes |
| |
| @staticmethod |
| def commitIgnored(subject, dir=None, files=None, d=None): |
| if files: |
| runcmd(['git', 'add'] + files, dir) |
| cmd = ["git"] |
| GitApplyTree.gitCommandUserOptions(cmd, d=d) |
| cmd += ["commit", "-m", subject, "--no-verify"] |
| runcmd(cmd, dir) |
| GitApplyTree.addNote(dir, "HEAD", GitApplyTree.ignore_commit) |
| |
| @staticmethod |
| def extractPatches(tree, startcommits, outdir, paths=None): |
| import tempfile |
| import shutil |
| tempdir = tempfile.mkdtemp(prefix='oepatch') |
| try: |
| for name, rev in startcommits.items(): |
| shellcmd = ["git", "format-patch", "--no-signature", "--no-numbered", rev, "-o", tempdir] |
| if paths: |
| shellcmd.append('--') |
| shellcmd.extend(paths) |
| out = runcmd(["sh", "-c", " ".join(shellcmd)], os.path.join(tree, name)) |
| if out: |
| for srcfile in out.split(): |
| # This loop, which is used to remove any line that |
| # starts with "%% original patch", is kept for backwards |
| # compatibility. If/when that compatibility is dropped, |
| # it can be replaced with code to just read the first |
| # line of the patch file to get the SHA-1, and the code |
| # below that writes the modified patch file can be |
| # replaced with a simple file move. |
| for encoding in ['utf-8', 'latin-1']: |
| patchlines = [] |
| try: |
| with open(srcfile, 'r', encoding=encoding, newline='') as f: |
| for line in f: |
| if line.startswith("%% " + GitApplyTree.original_patch): |
| continue |
| patchlines.append(line) |
| except UnicodeDecodeError: |
| continue |
| break |
| else: |
| raise PatchError('Unable to find a character encoding to decode %s' % srcfile) |
| |
| sha1 = patchlines[0].split()[1] |
| notes = GitApplyTree.getNotes(os.path.join(tree, name), sha1) |
| if GitApplyTree.ignore_commit in notes: |
| continue |
| outfile = notes.get(GitApplyTree.original_patch, os.path.basename(srcfile)) |
| |
| bb.utils.mkdirhier(os.path.join(outdir, name)) |
| with open(os.path.join(outdir, name, outfile), 'w') as of: |
| for line in patchlines: |
| of.write(line) |
| finally: |
| shutil.rmtree(tempdir) |
| |
| def _need_dirty_check(self): |
| fetch = bb.fetch2.Fetch([], self.d) |
| check_dirtyness = False |
| for url in fetch.urls: |
| url_data = fetch.ud[url] |
| parm = url_data.parm |
| # a git url with subpath param will surely be dirty |
| # since the git tree from which we clone will be emptied |
| # from all files that are not in the subpath |
| if url_data.type == 'git' and parm.get('subpath'): |
| check_dirtyness = True |
| return check_dirtyness |
| |
| def _commitpatch(self, patch, patchfilevar): |
| output = "" |
| # Add all files |
| shellcmd = ["git", "add", "-f", "-A", "."] |
| output += runcmd(["sh", "-c", " ".join(shellcmd)], self.dir) |
| # Exclude the patches directory |
| shellcmd = ["git", "reset", "HEAD", self.patchdir] |
| output += runcmd(["sh", "-c", " ".join(shellcmd)], self.dir) |
| # Commit the result |
| (tmpfile, shellcmd) = self.prepareCommit(patch['file'], self.commituser, self.commitemail) |
| try: |
| shellcmd.insert(0, patchfilevar) |
| output += runcmd(["sh", "-c", " ".join(shellcmd)], self.dir) |
| finally: |
| os.remove(tmpfile) |
| return output |
| |
| def _applypatch(self, patch, force = False, reverse = False, run = True): |
| import shutil |
| |
| def _applypatchhelper(shellcmd, patch, force = False, reverse = False, run = True): |
| if reverse: |
| shellcmd.append('-R') |
| |
| shellcmd.append(patch['file']) |
| |
| if not run: |
| return "sh" + "-c" + " ".join(shellcmd) |
| |
| return runcmd(["sh", "-c", " ".join(shellcmd)], self.dir) |
| |
| reporoot = (runcmd("git rev-parse --show-toplevel".split(), self.dir) or '').strip() |
| if not reporoot: |
| raise Exception("Cannot get repository root for directory %s" % self.dir) |
| |
| patch_applied = True |
| try: |
| patchfilevar = 'PATCHFILE="%s"' % os.path.basename(patch['file']) |
| if self._need_dirty_check(): |
| # Check dirtyness of the tree |
| try: |
| output = runcmd(["git", "--work-tree=%s" % reporoot, "status", "--short"]) |
| except CmdError: |
| pass |
| else: |
| if output: |
| # The tree is dirty, no need to try to apply patches with git anymore |
| # since they fail, fallback directly to patch |
| output = PatchTree._applypatch(self, patch, force, reverse, run) |
| output += self._commitpatch(patch, patchfilevar) |
| return output |
| try: |
| shellcmd = [patchfilevar, "git", "--work-tree=%s" % reporoot] |
| self.gitCommandUserOptions(shellcmd, self.commituser, self.commitemail) |
| shellcmd += ["am", "-3", "--keep-cr", "--no-scissors", "-p%s" % patch['strippath']] |
| return _applypatchhelper(shellcmd, patch, force, reverse, run) |
| except CmdError: |
| # Need to abort the git am, or we'll still be within it at the end |
| try: |
| shellcmd = ["git", "--work-tree=%s" % reporoot, "am", "--abort"] |
| runcmd(["sh", "-c", " ".join(shellcmd)], self.dir) |
| except CmdError: |
| pass |
| # git am won't always clean up after itself, sadly, so... |
| shellcmd = ["git", "--work-tree=%s" % reporoot, "reset", "--hard", "HEAD"] |
| runcmd(["sh", "-c", " ".join(shellcmd)], self.dir) |
| # Also need to take care of any stray untracked files |
| shellcmd = ["git", "--work-tree=%s" % reporoot, "clean", "-f"] |
| runcmd(["sh", "-c", " ".join(shellcmd)], self.dir) |
| |
| # Fall back to git apply |
| shellcmd = ["git", "--git-dir=%s" % reporoot, "apply", "-p%s" % patch['strippath']] |
| try: |
| output = _applypatchhelper(shellcmd, patch, force, reverse, run) |
| except CmdError: |
| # Fall back to patch |
| output = PatchTree._applypatch(self, patch, force, reverse, run) |
| output += self._commitpatch(patch, patchfilevar) |
| return output |
| except: |
| patch_applied = False |
| raise |
| finally: |
| if patch_applied: |
| GitApplyTree.addNote(self.dir, "HEAD", GitApplyTree.original_patch, os.path.basename(patch['file'])) |
| |
| |
| class QuiltTree(PatchSet): |
| def _runcmd(self, args, run = True): |
| quiltrc = self.d.getVar('QUILTRCFILE') |
| if not run: |
| return ["quilt"] + ["--quiltrc"] + [quiltrc] + args |
| runcmd(["quilt"] + ["--quiltrc"] + [quiltrc] + args, self.dir) |
| |
| def _quiltpatchpath(self, file): |
| return os.path.join(self.dir, "patches", os.path.basename(file)) |
| |
| |
| def __init__(self, dir, d): |
| PatchSet.__init__(self, dir, d) |
| self.initialized = False |
| p = os.path.join(self.dir, 'patches') |
| if not os.path.exists(p): |
| os.makedirs(p) |
| |
| def Clean(self): |
| try: |
| # make sure that patches/series file exists before quilt pop to keep quilt-0.67 happy |
| open(os.path.join(self.dir, "patches","series"), 'a').close() |
| self._runcmd(["pop", "-a", "-f"]) |
| oe.path.remove(os.path.join(self.dir, "patches","series")) |
| except Exception: |
| pass |
| self.initialized = True |
| |
| def InitFromDir(self): |
| # read series -> self.patches |
| seriespath = os.path.join(self.dir, 'patches', 'series') |
| if not os.path.exists(self.dir): |
| raise NotFoundError(self.dir) |
| if os.path.exists(seriespath): |
| with open(seriespath, 'r') as f: |
| for line in f.readlines(): |
| patch = {} |
| parts = line.strip().split() |
| patch["quiltfile"] = self._quiltpatchpath(parts[0]) |
| patch["quiltfilemd5"] = bb.utils.md5_file(patch["quiltfile"]) |
| if len(parts) > 1: |
| patch["strippath"] = parts[1][2:] |
| self.patches.append(patch) |
| |
| # determine which patches are applied -> self._current |
| try: |
| output = runcmd(["quilt", "applied"], self.dir) |
| except CmdError: |
| import sys |
| if sys.exc_value.output.strip() == "No patches applied": |
| return |
| else: |
| raise |
| output = [val for val in output.split('\n') if not val.startswith('#')] |
| for patch in self.patches: |
| if os.path.basename(patch["quiltfile"]) == output[-1]: |
| self._current = self.patches.index(patch) |
| self.initialized = True |
| |
| def Import(self, patch, force = None): |
| if not self.initialized: |
| self.InitFromDir() |
| PatchSet.Import(self, patch, force) |
| oe.path.symlink(patch["file"], self._quiltpatchpath(patch["file"]), force=True) |
| with open(os.path.join(self.dir, "patches", "series"), "a") as f: |
| f.write(os.path.basename(patch["file"]) + " -p" + patch["strippath"] + "\n") |
| patch["quiltfile"] = self._quiltpatchpath(patch["file"]) |
| patch["quiltfilemd5"] = bb.utils.md5_file(patch["quiltfile"]) |
| |
| # TODO: determine if the file being imported: |
| # 1) is already imported, and is the same |
| # 2) is already imported, but differs |
| |
| self.patches.insert(self._current or 0, patch) |
| |
| |
| def Push(self, force = False, all = False, run = True): |
| # quilt push [-f] |
| |
| args = ["push"] |
| if force: |
| args.append("-f") |
| if all: |
| args.append("-a") |
| if not run: |
| return self._runcmd(args, run) |
| |
| self._runcmd(args) |
| |
| if self._current is not None: |
| self._current = self._current + 1 |
| else: |
| self._current = 0 |
| |
| def Pop(self, force = None, all = None): |
| # quilt pop [-f] |
| args = ["pop"] |
| if force: |
| args.append("-f") |
| if all: |
| args.append("-a") |
| |
| self._runcmd(args) |
| |
| if self._current == 0: |
| self._current = None |
| |
| if self._current is not None: |
| self._current = self._current - 1 |
| |
| def Refresh(self, **kwargs): |
| if kwargs.get("remote"): |
| patch = self.patches[kwargs["patch"]] |
| if not patch: |
| raise PatchError("No patch found at index %s in patchset." % kwargs["patch"]) |
| (type, host, path, user, pswd, parm) = bb.fetch.decodeurl(patch["remote"]) |
| if type == "file": |
| import shutil |
| if not patch.get("file") and patch.get("remote"): |
| patch["file"] = bb.fetch2.localpath(patch["remote"], self.d) |
| |
| shutil.copyfile(patch["quiltfile"], patch["file"]) |
| else: |
| raise PatchError("Unable to do a remote refresh of %s, unsupported remote url scheme %s." % (os.path.basename(patch["quiltfile"]), type)) |
| else: |
| # quilt refresh |
| args = ["refresh"] |
| if kwargs.get("quiltfile"): |
| args.append(os.path.basename(kwargs["quiltfile"])) |
| elif kwargs.get("patch"): |
| args.append(os.path.basename(self.patches[kwargs["patch"]]["quiltfile"])) |
| self._runcmd(args) |
| |
| class Resolver(object): |
| def __init__(self, patchset, terminal): |
| raise NotImplementedError() |
| |
| def Resolve(self): |
| raise NotImplementedError() |
| |
| def Revert(self): |
| raise NotImplementedError() |
| |
| def Finalize(self): |
| raise NotImplementedError() |
| |
| class NOOPResolver(Resolver): |
| def __init__(self, patchset, terminal): |
| self.patchset = patchset |
| self.terminal = terminal |
| |
| def Resolve(self): |
| olddir = os.path.abspath(os.curdir) |
| os.chdir(self.patchset.dir) |
| try: |
| self.patchset.Push() |
| except Exception: |
| import sys |
| raise |
| finally: |
| os.chdir(olddir) |
| |
| # Patch resolver which relies on the user doing all the work involved in the |
| # resolution, with the exception of refreshing the remote copy of the patch |
| # files (the urls). |
| class UserResolver(Resolver): |
| def __init__(self, patchset, terminal): |
| self.patchset = patchset |
| self.terminal = terminal |
| |
| # Force a push in the patchset, then drop to a shell for the user to |
| # resolve any rejected hunks |
| def Resolve(self): |
| olddir = os.path.abspath(os.curdir) |
| os.chdir(self.patchset.dir) |
| try: |
| self.patchset.Push(False) |
| except CmdError as v: |
| # Patch application failed |
| patchcmd = self.patchset.Push(True, False, False) |
| |
| t = self.patchset.d.getVar('T') |
| if not t: |
| bb.msg.fatal("Build", "T not set") |
| bb.utils.mkdirhier(t) |
| import random |
| rcfile = "%s/bashrc.%s.%s" % (t, str(os.getpid()), random.random()) |
| with open(rcfile, "w") as f: |
| f.write("echo '*** Manual patch resolution mode ***'\n") |
| f.write("echo 'Dropping to a shell, so patch rejects can be fixed manually.'\n") |
| f.write("echo 'Run \"quilt refresh\" when patch is corrected, press CTRL+D to exit.'\n") |
| f.write("echo ''\n") |
| f.write(" ".join(patchcmd) + "\n") |
| os.chmod(rcfile, 0o775) |
| |
| self.terminal("bash --rcfile " + rcfile, 'Patch Rejects: Please fix patch rejects manually', self.patchset.d) |
| |
| # Construct a new PatchSet after the user's changes, compare the |
| # sets, checking patches for modifications, and doing a remote |
| # refresh on each. |
| oldpatchset = self.patchset |
| self.patchset = oldpatchset.__class__(self.patchset.dir, self.patchset.d) |
| |
| for patch in self.patchset.patches: |
| oldpatch = None |
| for opatch in oldpatchset.patches: |
| if opatch["quiltfile"] == patch["quiltfile"]: |
| oldpatch = opatch |
| |
| if oldpatch: |
| patch["remote"] = oldpatch["remote"] |
| if patch["quiltfile"] == oldpatch["quiltfile"]: |
| if patch["quiltfilemd5"] != oldpatch["quiltfilemd5"]: |
| bb.note("Patch %s has changed, updating remote url %s" % (os.path.basename(patch["quiltfile"]), patch["remote"])) |
| # user change? remote refresh |
| self.patchset.Refresh(remote=True, patch=self.patchset.patches.index(patch)) |
| else: |
| # User did not fix the problem. Abort. |
| raise PatchError("Patch application failed, and user did not fix and refresh the patch.") |
| except Exception: |
| raise |
| finally: |
| os.chdir(olddir) |
| |
| |
| def patch_path(url, fetch, workdir, expand=True): |
| """Return the local path of a patch, or return nothing if this isn't a patch""" |
| |
| local = fetch.localpath(url) |
| if os.path.isdir(local): |
| return |
| base, ext = os.path.splitext(os.path.basename(local)) |
| if ext in ('.gz', '.bz2', '.xz', '.Z'): |
| if expand: |
| local = os.path.join(workdir, base) |
| ext = os.path.splitext(base)[1] |
| |
| urldata = fetch.ud[url] |
| if "apply" in urldata.parm: |
| apply = oe.types.boolean(urldata.parm["apply"]) |
| if not apply: |
| return |
| elif ext not in (".diff", ".patch"): |
| return |
| |
| return local |
| |
| def src_patches(d, all=False, expand=True): |
| workdir = d.getVar('WORKDIR') |
| fetch = bb.fetch2.Fetch([], d) |
| patches = [] |
| sources = [] |
| for url in fetch.urls: |
| local = patch_path(url, fetch, workdir, expand) |
| if not local: |
| if all: |
| local = fetch.localpath(url) |
| sources.append(local) |
| continue |
| |
| urldata = fetch.ud[url] |
| parm = urldata.parm |
| patchname = parm.get('pname') or os.path.basename(local) |
| |
| apply, reason = should_apply(parm, d) |
| if not apply: |
| if reason: |
| bb.note("Patch %s %s" % (patchname, reason)) |
| continue |
| |
| patchparm = {'patchname': patchname} |
| if "striplevel" in parm: |
| striplevel = parm["striplevel"] |
| elif "pnum" in parm: |
| #bb.msg.warn(None, "Deprecated usage of 'pnum' url parameter in '%s', please use 'striplevel'" % url) |
| striplevel = parm["pnum"] |
| else: |
| striplevel = '1' |
| patchparm['striplevel'] = striplevel |
| |
| patchdir = parm.get('patchdir') |
| if patchdir: |
| patchparm['patchdir'] = patchdir |
| |
| localurl = bb.fetch.encodeurl(('file', '', local, '', '', patchparm)) |
| patches.append(localurl) |
| |
| if all: |
| return sources |
| |
| return patches |
| |
| |
| def should_apply(parm, d): |
| import bb.utils |
| if "mindate" in parm or "maxdate" in parm: |
| pn = d.getVar('PN') |
| srcdate = d.getVar('SRCDATE_%s' % pn) |
| if not srcdate: |
| srcdate = d.getVar('SRCDATE') |
| |
| if srcdate == "now": |
| srcdate = d.getVar('DATE') |
| |
| if "maxdate" in parm and parm["maxdate"] < srcdate: |
| return False, 'is outdated' |
| |
| if "mindate" in parm and parm["mindate"] > srcdate: |
| return False, 'is predated' |
| |
| |
| if "minrev" in parm: |
| srcrev = d.getVar('SRCREV') |
| if srcrev and srcrev < parm["minrev"]: |
| return False, 'applies to later revisions' |
| |
| if "maxrev" in parm: |
| srcrev = d.getVar('SRCREV') |
| if srcrev and srcrev > parm["maxrev"]: |
| return False, 'applies to earlier revisions' |
| |
| if "rev" in parm: |
| srcrev = d.getVar('SRCREV') |
| if srcrev and parm["rev"] not in srcrev: |
| return False, "doesn't apply to revision" |
| |
| if "notrev" in parm: |
| srcrev = d.getVar('SRCREV') |
| if srcrev and parm["notrev"] in srcrev: |
| return False, "doesn't apply to revision" |
| |
| if "maxver" in parm: |
| pv = d.getVar('PV') |
| if bb.utils.vercmp_string_op(pv, parm["maxver"], ">"): |
| return False, "applies to earlier version" |
| |
| if "minver" in parm: |
| pv = d.getVar('PV') |
| if bb.utils.vercmp_string_op(pv, parm["minver"], "<"): |
| return False, "applies to later version" |
| |
| return True, None |