| import oe.path |
| |
| 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, exitstatus, output): |
| self.status = exitstatus |
| self.output = output |
| |
| def __str__(self): |
| return "Command Error: exit status: %d Output:\n%s" % (self.status, self.output) |
| |
| |
| def runcmd(args, dir = None): |
| import pipes |
| |
| 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 = [ pipes.quote(str(arg)) for arg in args ] |
| cmd = " ".join(args) |
| # print("cmd: %s" % cmd) |
| (exitstatus, output) = oe.utils.getstatusoutput(cmd) |
| if exitstatus != 0: |
| raise CmdError(exitstatus >> 8, output) |
| return output |
| |
| 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"] = bb.data.expand(bb.fetch2.localpath(patch["remote"], self.d), 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:]) |
| |
| 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)) |
| |
| 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", "-p", patch['strippath']] |
| if reverse: |
| shellcmd.append('-R') |
| |
| if not run: |
| return "sh" + "-c" + " ".join(shellcmd) |
| |
| if not force: |
| shellcmd.append('--dry-run') |
| |
| output = runcmd(["sh", "-c", " ".join(shellcmd)], self.dir) |
| |
| if force: |
| return |
| |
| shellcmd.pop(len(shellcmd) - 1) |
| output = runcmd(["sh", "-c", " ".join(shellcmd)], self.dir) |
| |
| 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): |
| patch_line_prefix = '%% original patch' |
| |
| def __init__(self, dir, d): |
| PatchTree.__init__(self, dir, d) |
| |
| @staticmethod |
| def extractPatchHeader(patchfile): |
| """ |
| Extract just the header lines from the top of a patch file |
| """ |
| lines = [] |
| with open(patchfile, 'r') as f: |
| for line in f.readlines(): |
| if line.startswith('Index: ') or line.startswith('diff -') or line.startswith('---'): |
| break |
| lines.append(line) |
| return lines |
| |
| @staticmethod |
| def prepareCommit(patchfile): |
| """ |
| 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 |
| import re |
| author_re = re.compile('[\S ]+ <\S+@\S+\.\S+>') |
| # Process patch header and extract useful information |
| lines = GitApplyTree.extractPatchHeader(patchfile) |
| outlines = [] |
| author = None |
| date = None |
| for line in lines: |
| if line.startswith('Subject: '): |
| subject = line.split(':', 1)[1] |
| # Remove any [PATCH][oe-core] etc. |
| subject = re.sub(r'\[.+?\]\s*', '', subject) |
| outlines.insert(0, '%s\n\n' % subject.strip()) |
| continue |
| if line.startswith('From: ') or line.startswith('Author: '): |
| authorval = line.split(':', 1)[1].strip().replace('"', '') |
| # git is fussy about author formatting i.e. it must be Name <email@domain> |
| if author_re.match(authorval): |
| author = authorval |
| continue |
| if 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 |
| if line.startswith('Signed-off-by: '): |
| authorval = line.split(':', 1)[1].strip().replace('"', '') |
| # git is fussy about author formatting i.e. it must be Name <email@domain> |
| if author_re.match(authorval): |
| author = authorval |
| outlines.append(line) |
| # 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", "commit", "-F", tmpfile] |
| # 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 extractPatches(tree, startcommit, outdir, paths=None): |
| import tempfile |
| import shutil |
| tempdir = tempfile.mkdtemp(prefix='oepatch') |
| try: |
| shellcmd = ["git", "format-patch", startcommit, "-o", tempdir] |
| if paths: |
| shellcmd.append('--') |
| shellcmd.extend(paths) |
| out = runcmd(["sh", "-c", " ".join(shellcmd)], tree) |
| if out: |
| for srcfile in out.split(): |
| patchlines = [] |
| outfile = None |
| with open(srcfile, 'r') as f: |
| for line in f: |
| if line.startswith(GitApplyTree.patch_line_prefix): |
| outfile = line.split()[-1].strip() |
| continue |
| patchlines.append(line) |
| if not outfile: |
| outfile = os.path.basename(srcfile) |
| with open(os.path.join(outdir, outfile), 'w') as of: |
| for line in patchlines: |
| of.write(line) |
| finally: |
| shutil.rmtree(tempdir) |
| |
| 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) |
| |
| # Add hooks which add a pointer to the original patch file name in the commit message |
| 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) |
| commithook = os.path.join(reporoot, '.git', 'hooks', 'commit-msg') |
| commithook_backup = commithook + '.devtool-orig' |
| applyhook = os.path.join(reporoot, '.git', 'hooks', 'applypatch-msg') |
| applyhook_backup = applyhook + '.devtool-orig' |
| if os.path.exists(commithook): |
| shutil.move(commithook, commithook_backup) |
| if os.path.exists(applyhook): |
| shutil.move(applyhook, applyhook_backup) |
| with open(commithook, 'w') as f: |
| # NOTE: the formatting here is significant; if you change it you'll also need to |
| # change other places which read it back |
| f.write('echo >> $1\n') |
| f.write('echo "%s: $PATCHFILE" >> $1\n' % GitApplyTree.patch_line_prefix) |
| os.chmod(commithook, 0755) |
| shutil.copy2(commithook, applyhook) |
| try: |
| patchfilevar = 'PATCHFILE="%s"' % os.path.basename(patch['file']) |
| try: |
| shellcmd = [patchfilevar, "git", "--work-tree=%s" % reporoot, "am", "-3", "--keep-cr", "-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) |
| # 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']) |
| try: |
| shellcmd.insert(0, patchfilevar) |
| output += runcmd(["sh", "-c", " ".join(shellcmd)], self.dir) |
| finally: |
| os.remove(tmpfile) |
| return output |
| finally: |
| os.remove(commithook) |
| os.remove(applyhook) |
| if os.path.exists(commithook_backup): |
| shutil.move(commithook_backup, commithook) |
| if os.path.exists(applyhook_backup): |
| shutil.move(applyhook_backup, applyhook) |
| |
| |
| class QuiltTree(PatchSet): |
| def _runcmd(self, args, run = True): |
| quiltrc = self.d.getVar('QUILTRCFILE', True) |
| 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: |
| 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 |
| os.chdir(olddir) |
| raise |
| |
| # 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', True) |
| 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, 0775) |
| |
| 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: |
| os.chdir(olddir) |
| raise |
| os.chdir(olddir) |