[yocto] [[AUH] 04/17] upgradehelper: Reorder files into directories.

Aníbal Limón anibal.limon at linux.intel.com
Wed Nov 25 16:00:33 PST 2015


Reorder files into modules directory to provide a better
source tree.

Changed was made for update the import lines and include
modules directory into search path.

Signed-off-by: Aníbal Limón <anibal.limon at linux.intel.com>
---
 bitbake.py                    | 124 --------
 buildhistory.py               |  75 -----
 emailhandler.py               | 108 -------
 errors.py                     |  93 ------
 git.py                        | 101 -------
 gitrecipe.py                  |  94 ------
 modules/buildhistory.py       |  75 +++++
 modules/errors.py             |  93 ++++++
 modules/recipe/__init__.py    |   0
 modules/recipe/base.py        | 671 ++++++++++++++++++++++++++++++++++++++++++
 modules/recipe/git.py         |  94 ++++++
 modules/recipe/svn.py         |  28 ++
 modules/statistics.py         | 102 +++++++
 modules/steps.py              | 143 +++++++++
 modules/utils/__init__.py     |   0
 modules/utils/bitbake.py      | 124 ++++++++
 modules/utils/emailhandler.py | 108 +++++++
 modules/utils/git.py          | 102 +++++++
 recipe.py                     | 669 -----------------------------------------
 statistics.py                 | 102 -------
 steps.py                      | 142 ---------
 svnrecipe.py                  |  28 --
 upgradehelper.py              |  11 +-
 23 files changed, 1547 insertions(+), 1540 deletions(-)
 delete mode 100644 bitbake.py
 delete mode 100644 buildhistory.py
 delete mode 100644 emailhandler.py
 delete mode 100644 errors.py
 delete mode 100644 git.py
 delete mode 100644 gitrecipe.py
 create mode 100644 modules/buildhistory.py
 create mode 100644 modules/errors.py
 create mode 100644 modules/recipe/__init__.py
 create mode 100644 modules/recipe/base.py
 create mode 100644 modules/recipe/git.py
 create mode 100644 modules/recipe/svn.py
 create mode 100644 modules/statistics.py
 create mode 100644 modules/steps.py
 create mode 100644 modules/utils/__init__.py
 create mode 100644 modules/utils/bitbake.py
 create mode 100644 modules/utils/emailhandler.py
 create mode 100644 modules/utils/git.py
 delete mode 100644 recipe.py
 delete mode 100644 statistics.py
 delete mode 100644 steps.py
 delete mode 100644 svnrecipe.py

diff --git a/bitbake.py b/bitbake.py
deleted file mode 100644
index cdbce2b..0000000
--- a/bitbake.py
+++ /dev/null
@@ -1,124 +0,0 @@
-#!/usr/bin/env python
-# vim: set ts=4 sw=4 et:
-#
-# Copyright (c) 2013 - 2014 Intel Corporation
-#
-# This program is free software; you can redistribute it and/or
-# modify it under the terms of the GNU General Public License
-# as published by the Free Software Foundation; either version 2
-# of the License, or (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
-#
-# AUTHORS
-# Laurentiu Palcu   <laurentiu.palcu at intel.com>
-# Marius Avram      <marius.avram at intel.com>
-#
-
-import os
-import logging as log
-from logging import info as I
-from logging import debug as D
-from logging import error as E
-from logging import critical as C
-import sys
-import re
-
-from errors import *
-
-for path in os.environ["PATH"].split(':'):
-    if os.path.exists(path) and "bitbake" in os.listdir(path):
-        sys.path.insert(0, os.path.join(path, "../lib"))
-        import bb
-
-BITBAKE_ERROR_LOG = 'bitbake_error_log.txt'
-
-class Bitbake(object):
-    def __init__(self, build_dir):
-        self.build_dir = build_dir
-        self.log_dir = None
-        super(Bitbake, self).__init__()
-
-    def _cmd(self, recipe=None, options=None, env_var=None, output_filter=None):
-        cmd = ""
-        if env_var is not None:
-            cmd += env_var + " "
-        cmd += "bitbake "
-        if options is not None:
-            cmd += options + " "
-
-        if recipe is not None:
-            cmd += recipe
-
-        if output_filter is not None:
-            cmd += ' |  grep ' + output_filter
-
-        os.chdir(self.build_dir)
-
-        try:
-            stdout, stderr = bb.process.run(cmd)
-        except bb.process.ExecutionError as e:
-            D("%s returned:\n%s" % (cmd, e.__str__()))
-
-            if self.log_dir is not None and os.path.exists(self.log_dir):
-                with open(os.path.join(self.log_dir, BITBAKE_ERROR_LOG), "w+") as log:
-                    log.write(e.stdout)
-
-            raise Error("\'" + cmd + "\' failed", e.stdout, e.stderr)
-
-        return stdout
-
-    def set_log_dir(self, dir):
-        self.log_dir = dir
-
-    def get_stdout_log(self):
-        return os.path.join(self.log_dir, BITBAKE_ERROR_LOG)
-
-    def env(self, recipe=None):
-        stdout = self._cmd(recipe, "-e", output_filter="-v \"^#\"")
-
-        assignment = re.compile("^([^ \t=]*)=(.*)")
-        bb_env = dict()
-        for line in stdout.split('\n'):
-            m = assignment.match(line)
-            if m:
-                if m.group(1) in bb_env:
-                    continue
-
-                bb_env[m.group(1)] = m.group(2).strip("\"")
-
-        if not bb_env:
-            raise EmptyEnvError(stdout)
-
-        return bb_env
-
-    def fetch(self, recipe):
-        return self._cmd(recipe, "-c fetch")
-
-    def unpack(self, recipe):
-        return self._cmd(recipe, "-c unpack")
-
-    def checkpkg(self, recipe):
-        if recipe == "universe":
-            return self._cmd(recipe, "-c checkpkg -k")
-        else:
-            return self._cmd(recipe, "-c checkpkg")
-
-    def cleanall(self, recipe):
-        return self._cmd(recipe, "-c cleanall")
-
-    def cleansstate(self, recipe):
-        return self._cmd(recipe, "-c cleansstate")
-
-    def complete(self, recipe, machine):
-        return self._cmd(recipe, env_var="MACHINE=" + machine)
-
-    def dependency_graph(self, package_list):
-        return self._cmd(package_list, "-g")
diff --git a/buildhistory.py b/buildhistory.py
deleted file mode 100644
index 842efc3..0000000
--- a/buildhistory.py
+++ /dev/null
@@ -1,75 +0,0 @@
-#!/usr/bin/env python
-# vim: set ts=4 sw=4 et:
-#
-# Copyright (c) 2015 Intel Corporation
-#
-# This program is free software; you can redistribute it and/or
-# modify it under the terms of the GNU General Public License
-# as published by the Free Software Foundation; either version 2
-# of the License, or (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
-#
-
-import os
-import logging as log
-from logging import debug as D
-from logging import info as I
-from logging import warning as W
-from logging import error as E
-from logging import critical as C
-import sys
-from errors import *
-
-from bitbake import *
-from git import Git
-
-class BuildHistory(object):
-    def __init__(self, bb, pn, workdir):
-        self.bb = bb
-        self.pn = pn
-        self.workdir = workdir
-        self.revs = []
-
-        self.buildhistory_dir = os.path.join(self.workdir, 'buildhistory')
-        if not os.path.exists(self.buildhistory_dir):
-            os.mkdir(self.buildhistory_dir)
-
-        self.git = Git(self.buildhistory_dir)
-
-        os.environ['BB_ENV_EXTRAWHITE'] = os.environ['BB_ENV_EXTRAWHITE'] + \
-                                    " BUILDHISTORY_DIR"
-        os.environ["BUILDHISTORY_DIR"] = self.buildhistory_dir
-
-    def init(self, machines):
-        self.bb.cleanall(self.pn)
-        for machine in machines:
-            self.bb.complete(self.pn, machine)
-            self.revs.append(self.git.last_commit("master"))
-
-    def add(self):
-        self.revs.append(self.git.last_commit("master"))
-
-    def diff(self):
-        rev_initial = self.revs[0]
-        rev_final = self.revs[-1]
-
-        cmd = "buildhistory-diff -a -p %s %s %s"  % (self.buildhistory_dir, 
-                rev_initial, rev_final)
-
-        try:
-            stdout, stderr = bb.process.run(cmd)
-
-            if stdout and os.path.exists(self.workdir):
-                with open(os.path.join(self.workdir, "buildhistory-diff.txt"),
-                        "w+") as log:
-                    log.write(stdout)
-        except bb.process.ExecutionError as e:
-            W( "%s: Buildhistory checking fails\n%s" % (self.pn, e.stdout))
diff --git a/emailhandler.py b/emailhandler.py
deleted file mode 100644
index 970fb59..0000000
--- a/emailhandler.py
+++ /dev/null
@@ -1,108 +0,0 @@
-#!/usr/bin/env python
-# vim: set ts=4 sw=4 et:
-#
-# Copyright (c) 2013 - 2014 Intel Corporation
-#
-# This program is free software; you can redistribute it and/or
-# modify it under the terms of the GNU General Public License
-# as published by the Free Software Foundation; either version 2
-# of the License, or (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
-#
-# AUTHORS
-# Laurentiu Palcu   <laurentiu.palcu at intel.com>
-# Marius Avram      <marius.avram at intel.com>
-#
-
-import os
-import logging as log
-from logging import error as E
-from logging import info as I
-from smtplib import SMTP
-import mimetypes
-from email.mime.text import MIMEText
-from email.mime.base import MIMEBase
-from email.mime.multipart import MIMEMultipart
-from email.generator import Generator
-import shutil
-from cStringIO import StringIO
-
-class Email(object):
-    def __init__(self, settings):
-        self.smtp_host = None
-        self.smtp_port = None
-        self.from_addr = None
-        if "smtp" in settings:
-            smtp_entry = settings["smtp"].split(":")
-            if len(smtp_entry) == 1:
-                self.smtp_host = smtp_entry[0]
-                self.smtp_port = 25
-            elif len(smtp_entry) == 2:
-                self.smtp_host = smtp_entry[0]
-                self.smtp_port = smtp_entry[1]
-        else:
-            E(" smtp host not set! Sending emails disabled!")
-
-        if "from" in settings:
-            self.from_addr = settings["from"]
-        else:
-            E(" 'From' address not set! Sending emails disabled!")
-
-        super(Email, self).__init__()
-
-    def send_email(self, to_addr, subject, text, files=[], cc_addr=None):
-        if self.smtp_host is None or self.from_addr is None:
-            return 0
-
-        I(" Sending email to: %s" % to_addr)
-
-        msg = MIMEMultipart()
-        msg['From'] = self.from_addr
-        if type(to_addr) is list:
-            msg['To'] = ', '.join(to_addr)
-        else:
-            msg['To'] = to_addr
-        if cc_addr is not None:
-            if type(cc_addr) is list:
-                msg['Cc'] = ', '.join(cc_addr)
-            else:
-                msg['Cc'] = cc_addr
-        msg['Subject'] = subject
-
-        msg.attach(MIMEText(text))
-
-        for file in files:
-            ctype, encoding = mimetypes.guess_type(file)
-            if ctype is None or encoding is not None:
-                ctype = 'application/octet-stream'
-            maintype, subtype = ctype.split('/', 1)
-
-            if maintype == "text":
-                attachment = MIMEText(open(file).read(), _subtype=subtype)
-            else:
-                attachment = MIMEBase(maintype, _subtype=subtype)
-                attachment.set_payload(open(file, 'rb').read())
-
-            attachment.add_header('Content-Disposition', 'attachment; filename="%s"'
-                                  % os.path.basename(file))
-            msg.attach(attachment)
-
-        out = StringIO()
-        Generator(out, mangle_from_=False).flatten(msg)
-        msg_text = out.getvalue()
-
-        try:
-            smtp = SMTP(self.smtp_host, self.smtp_port)
-            smtp.sendmail(self.from_addr, to_addr, msg_text)
-            smtp.close()
-        except Exception as e:
-            E("Could not send email: %s" % str(e))
-
diff --git a/errors.py b/errors.py
deleted file mode 100644
index 1504fa5..0000000
--- a/errors.py
+++ /dev/null
@@ -1,93 +0,0 @@
-#!/usr/bin/env python
-# vim: set ts=4 sw=4 et:
-#
-# Copyright (c) 2013 - 2014 Intel Corporation
-#
-# This program is free software; you can redistribute it and/or
-# modify it under the terms of the GNU General Public License
-# as published by the Free Software Foundation; either version 2
-# of the License, or (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
-#
-# AUTHORS
-# Laurentiu Palcu   <laurentiu.palcu at intel.com>
-# Marius Avram      <marius.avram at intel.com>
-#
-
-class Error(Exception):
-    def __init__(self, message=None, stdout=None, stderr=None):
-        self.message = message
-        self.stdout = stdout
-        self.stderr = stderr
-
-    def __str__(self):
-        return "Failed(other errors)"
-
-class MaintainerError(Error):
-    """ Class for group error that can be sent to Maintainer's """
-    def __init__(self, message=None, stdout=None, stderr=None):
-        super(MaintainerError, self).__init__(message, stdout, stderr)
-
-class FetchError(Error):
-    def __init__(self):
-        super(FetchError, self).__init__("do_fetch failed")
-
-    def __str__(self):
-        return "Failed(do_fetch)"
-
-class PatchError(MaintainerError):
-    def __init__(self):
-        super(PatchError, self).__init__("do_patch failed")
-
-    def __str__(self):
-        return "Failed(do_patch)"
-
-class ConfigureError(MaintainerError):
-    def __init__(self):
-        super(ConfigureError, self).__init__("do_configure failed")
-
-    def __str__(self):
-        return "Failed(do_configure)"
-
-class CompilationError(MaintainerError):
-    def __init__(self):
-        super(CompilationError, self).__init__("do_compile failed")
-
-    def __str__(self):
-        return "Failed(do_compile)"
-
-class LicenseError(MaintainerError):
-    def __init__(self):
-        super(LicenseError, self).__init__("license checksum does not match")
-
-    def __str__(self):
-        return "Failed(license issue)"
-
-class UnsupportedProtocolError(Error):
-    def __init__(self):
-        super(UnsupportedProtocolError, self).__init__("SRC_URI protocol not supported")
-
-    def __str__(self):
-        return "Failed(Unsupported protocol)"
-
-class UpgradeNotNeededError(Error):
-    def __init__(self):
-        super(UpgradeNotNeededError, self).__init__("Recipe already up to date")
-
-    def __str__(self):
-        return "Failed(up to date)"
-
-class EmptyEnvError(Error):
-    def __init__(self, stdout):
-        super(EmptyEnvError, self).__init__("Empty environment returned", stdout)
-
-    def __str__(self):
-        return "Failed(get_env)"
diff --git a/git.py b/git.py
deleted file mode 100644
index 9661364..0000000
--- a/git.py
+++ /dev/null
@@ -1,101 +0,0 @@
-#!/usr/bin/env python
-# vim: set ts=4 sw=4 et:
-#
-# Copyright (c) 2013 - 2014 Intel Corporation
-#
-# This program is free software; you can redistribute it and/or
-# modify it under the terms of the GNU General Public License
-# as published by the Free Software Foundation; either version 2
-# of the License, or (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
-#
-# AUTHORS
-# Laurentiu Palcu   <laurentiu.palcu at intel.com>
-# Marius Avram      <marius.avram at intel.com>
-#
-
-import os
-import logging as log
-from logging import debug as D
-from bitbake import *
-
-class Git(object):
-    def __init__(self, dir):
-        self.repo_dir = dir
-        super(Git, self).__init__()
-
-    def _cmd(self, operation):
-        os.chdir(self.repo_dir)
-
-        cmd = "git " + operation
-        try:
-            stdout, stderr = bb.process.run(cmd)
-        except bb.process.ExecutionError as e:
-            D("%s returned:\n%s" % (cmd, e.__str__()))
-            raise Error("The following git command failed: " + operation,
-                        e.stdout, e.stderr)
-
-        return stdout
-
-    def mv(self, src, dest):
-        return self._cmd("mv -f " + src + " " + dest)
-
-    def stash(self):
-        return self._cmd("stash")
-
-    def commit(self, commit_message, author=None):
-        if author is None:
-            return self._cmd("commit -a -s -m \"" + commit_message + "\"")
-        else:
-            return self._cmd("commit -a --author=\"" + author + "\" -m \"" + commit_message + "\"")
-
-    def create_patch(self, out_dir):
-        return self._cmd("format-patch -M10 -1 -o " + out_dir)
-
-    def status(self):
-        return self._cmd("status --porcelain")
-
-    def checkout_branch(self, branch_name):
-        return self._cmd("checkout " + branch_name)
-
-    def create_branch(self, branch_name):
-        return self._cmd("checkout -b " + branch_name)
-
-    def delete_branch(self, branch_name):
-        return self._cmd("branch -D " + branch_name)
-
-    def pull(self):
-        return self._cmd("pull")
-
-    def reset_hard(self, no_of_patches=0):
-        if no_of_patches == 0:
-            return self._cmd("reset --hard HEAD")
-        else:
-            return self._cmd("reset --hard HEAD~" + str(no_of_patches))
-
-    def reset_soft(self, no_of_patches):
-        return self._cmd("reset --soft HEAD~" + str(no_of_patches))
-
-    def clean_untracked(self):
-        return self._cmd("clean -fd")
-
-    def last_commit(self, branch_name):
-        return self._cmd("log --pretty=format:\"%H\" -1 " + branch_name)
-
-    def ls_remote(self, repo_url=None, options=None, refs=None):
-        cmd = "ls-remote"
-        if options is not None:
-            cmd += " " + options
-        if repo_url is not None:
-            cmd += " " + repo_url
-        if refs is not None:
-            cmd += " " + refs
-        return self._cmd(cmd)
diff --git a/gitrecipe.py b/gitrecipe.py
deleted file mode 100644
index dfa5cd2..0000000
--- a/gitrecipe.py
+++ /dev/null
@@ -1,94 +0,0 @@
-#!/usr/bin/env python
-# vim: set ts=4 sw=4 et:
-#
-# Copyright (c) 2013 - 2014 Intel Corporation
-#
-# This program is free software; you can redistribute it and/or
-# modify it under the terms of the GNU General Public License
-# as published by the Free Software Foundation; either version 2
-# of the License, or (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
-#
-# AUTHORS
-# Laurentiu Palcu   <laurentiu.palcu at intel.com>
-# Marius Avram      <marius.avram at intel.com>
-#
-
-from recipe import *
-
-class GitRecipe(Recipe):
-    def _extract_tag_from_ver(self, ver):
-        m = re.match("(.*)\+.*\+.*", ver)
-        if m is not None:
-            return m.group(1)
-
-        # allow errors in the reporting system
-        return ver
-
-    def _get_tag_sha1(self, new_tag):
-        m = re.match(".*(git://[^ ;]*).*", self.env['SRC_URI'])
-        if m is None:
-            raise Error("could not extract repo url from SRC_URI")
-
-        repo_url = m.group(1)
-        tags = self.git.ls_remote(repo_url, "--tags")
-
-        # Try to find tag ending with ^{}
-        for tag in tags.split('\n'):
-            if tag.endswith(new_tag + "^{}"):
-                return tag.split()[0]
-
-        # If not found, try to find simple tag
-        for tag in tags.split('\n'):
-            if tag.endswith(new_tag):
-                return tag.split()[0]
-
-        return None
-
-    def rename(self):
-        old_git_tag = self._extract_tag_from_ver(self.env['PKGV'])
-        new_git_tag = self._extract_tag_from_ver(self.new_ver)
-
-        if new_git_tag == old_git_tag:
-            raise UpgradeNotNeededError()
-
-        tag_sha1 = self._get_tag_sha1(new_git_tag)
-        if tag_sha1 is None:
-            raise Error("could not extract tag sha1")
-
-        for f in os.listdir(self.recipe_dir):
-            full_path_f = os.path.join(self.recipe_dir, f)
-            if os.path.isfile(full_path_f) and \
-                    ((f.find(self.env['PN']) == 0 and (f.find(old_git_tag) != -1 or
-                      f.find("git") != -1) and f.find(".bb") != -1) or
-                     (f.find(self.env['PN']) == 0 and f.find(".inc") != -1)):
-                with open(full_path_f + ".tmp", "w+") as temp_recipe:
-                    with open(full_path_f) as recipe:
-                        for line in recipe:
-                            m1 = re.match("^SRCREV *= *\".*\"", line)
-                            m2 = re.match("PV *= *\"[^\+]*(.*)\"", line)
-                            if m1 is not None:
-                                temp_recipe.write("SRCREV = \"" + tag_sha1 + "\"\n")
-                            elif m2 is not None:
-                                temp_recipe.write("PV = \"" + new_git_tag + m2.group(1) + "\"\n")
-                            else:
-                                temp_recipe.write(line)
-
-                os.rename(full_path_f + ".tmp", full_path_f)
-
-        self.env['PKGV'] = old_git_tag
-        self.new_ver = new_git_tag
-
-        super(GitRecipe, self).rename()
-
-    def fetch(self):
-        pass
-
diff --git a/modules/buildhistory.py b/modules/buildhistory.py
new file mode 100644
index 0000000..d8ca46d
--- /dev/null
+++ b/modules/buildhistory.py
@@ -0,0 +1,75 @@
+#!/usr/bin/env python
+# vim: set ts=4 sw=4 et:
+#
+# Copyright (c) 2015 Intel Corporation
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+#
+
+import os
+import logging as log
+from logging import debug as D
+from logging import info as I
+from logging import warning as W
+from logging import error as E
+from logging import critical as C
+import sys
+
+from errors import *
+from utils.git import Git
+from utils.bitbake import *
+
+class BuildHistory(object):
+    def __init__(self, bb, pn, workdir):
+        self.bb = bb
+        self.pn = pn
+        self.workdir = workdir
+        self.revs = []
+
+        self.buildhistory_dir = os.path.join(self.workdir, 'buildhistory')
+        if not os.path.exists(self.buildhistory_dir):
+            os.mkdir(self.buildhistory_dir)
+
+        self.git = Git(self.buildhistory_dir)
+
+        os.environ['BB_ENV_EXTRAWHITE'] = os.environ['BB_ENV_EXTRAWHITE'] + \
+                                    " BUILDHISTORY_DIR"
+        os.environ["BUILDHISTORY_DIR"] = self.buildhistory_dir
+
+    def init(self, machines):
+        self.bb.cleanall(self.pn)
+        for machine in machines:
+            self.bb.complete(self.pn, machine)
+            self.revs.append(self.git.last_commit("master"))
+
+    def add(self):
+        self.revs.append(self.git.last_commit("master"))
+
+    def diff(self):
+        rev_initial = self.revs[0]
+        rev_final = self.revs[-1]
+
+        cmd = "buildhistory-diff -a -p %s %s %s"  % (self.buildhistory_dir, 
+                rev_initial, rev_final)
+
+        try:
+            stdout, stderr = bb.process.run(cmd)
+
+            if stdout and os.path.exists(self.workdir):
+                with open(os.path.join(self.workdir, "buildhistory-diff.txt"),
+                        "w+") as log:
+                    log.write(stdout)
+        except bb.process.ExecutionError as e:
+            W( "%s: Buildhistory checking fails\n%s" % (self.pn, e.stdout))
diff --git a/modules/errors.py b/modules/errors.py
new file mode 100644
index 0000000..1504fa5
--- /dev/null
+++ b/modules/errors.py
@@ -0,0 +1,93 @@
+#!/usr/bin/env python
+# vim: set ts=4 sw=4 et:
+#
+# Copyright (c) 2013 - 2014 Intel Corporation
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+#
+# AUTHORS
+# Laurentiu Palcu   <laurentiu.palcu at intel.com>
+# Marius Avram      <marius.avram at intel.com>
+#
+
+class Error(Exception):
+    def __init__(self, message=None, stdout=None, stderr=None):
+        self.message = message
+        self.stdout = stdout
+        self.stderr = stderr
+
+    def __str__(self):
+        return "Failed(other errors)"
+
+class MaintainerError(Error):
+    """ Class for group error that can be sent to Maintainer's """
+    def __init__(self, message=None, stdout=None, stderr=None):
+        super(MaintainerError, self).__init__(message, stdout, stderr)
+
+class FetchError(Error):
+    def __init__(self):
+        super(FetchError, self).__init__("do_fetch failed")
+
+    def __str__(self):
+        return "Failed(do_fetch)"
+
+class PatchError(MaintainerError):
+    def __init__(self):
+        super(PatchError, self).__init__("do_patch failed")
+
+    def __str__(self):
+        return "Failed(do_patch)"
+
+class ConfigureError(MaintainerError):
+    def __init__(self):
+        super(ConfigureError, self).__init__("do_configure failed")
+
+    def __str__(self):
+        return "Failed(do_configure)"
+
+class CompilationError(MaintainerError):
+    def __init__(self):
+        super(CompilationError, self).__init__("do_compile failed")
+
+    def __str__(self):
+        return "Failed(do_compile)"
+
+class LicenseError(MaintainerError):
+    def __init__(self):
+        super(LicenseError, self).__init__("license checksum does not match")
+
+    def __str__(self):
+        return "Failed(license issue)"
+
+class UnsupportedProtocolError(Error):
+    def __init__(self):
+        super(UnsupportedProtocolError, self).__init__("SRC_URI protocol not supported")
+
+    def __str__(self):
+        return "Failed(Unsupported protocol)"
+
+class UpgradeNotNeededError(Error):
+    def __init__(self):
+        super(UpgradeNotNeededError, self).__init__("Recipe already up to date")
+
+    def __str__(self):
+        return "Failed(up to date)"
+
+class EmptyEnvError(Error):
+    def __init__(self, stdout):
+        super(EmptyEnvError, self).__init__("Empty environment returned", stdout)
+
+    def __str__(self):
+        return "Failed(get_env)"
diff --git a/modules/recipe/__init__.py b/modules/recipe/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/modules/recipe/base.py b/modules/recipe/base.py
new file mode 100644
index 0000000..14aa5bb
--- /dev/null
+++ b/modules/recipe/base.py
@@ -0,0 +1,671 @@
+#!/usr/bin/env python
+# vim: set ts=4 sw=4 et:
+#
+# Copyright (c) 2013 - 2014 Intel Corporation
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+#
+# AUTHORS
+# Laurentiu Palcu   <laurentiu.palcu at intel.com>
+# Marius Avram      <marius.avram at intel.com>
+#
+
+import os
+import re
+import sys
+import logging as log
+from logging import debug as D
+from logging import info as I
+from logging import warning as W
+
+from errors import *
+from utils.bitbake import *
+
+class Recipe(object):
+    def __init__(self, env, new_ver, interactive, workdir, recipe_dir, bitbake, git):
+        self.env = env
+        self.new_ver = new_ver
+        self.interactive = interactive
+        self.workdir = workdir
+        self.recipe_dir = recipe_dir
+        self.bb = bitbake
+        self.bb.set_log_dir(workdir)
+        self.git = git
+
+        self.retried_recipes = set()
+        self.license_diff_file = None
+
+        self.recipes_renamed = False
+        self.checksums_changed = False
+
+        self.removed_patches = False
+
+        self.suffixes = [
+            "tar.gz", "tgz", "zip", "tar.bz2", "tar.xz", "tar.lz4", "bz2",
+            "lz4", "orig.tar.gz", "src.tar.gz", "src.rpm", "src.tgz",
+            "svnr\d+.tar.bz2", "stable.tar.gz", "src.rpm"]
+        self.old_env = None
+
+        self.commit_msg = self.env['PN'] + ": upgrade to " + self.new_ver + "\n\n"
+        self.rm_patches_msg = "\n\nRemoved the following patch(es):\n"
+
+        super(Recipe, self).__init__()
+
+    def update_env(self, env):
+        self.env = env
+
+    def _rename_files_dir(self, old_ver, new_ver):
+        # The files directory is renamed only if the previous
+        # one has the following format PackageName-PackageVersion.
+        # Otherwise is kept the same way.
+        src_dir = os.path.join(self.recipe_dir, self.env['PN'] + "-" + old_ver)
+        dest_dir = os.path.join(self.recipe_dir, self.env['PN'] + "-" + new_ver)
+
+        if os.path.exists(src_dir) and os.path.isdir(src_dir):
+            self.git.mv(src_dir, dest_dir)
+
+    def rename(self):
+        # change PR before renaming
+        for f in os.listdir(self.recipe_dir):
+            full_path_f = os.path.join(self.recipe_dir, f)
+            if os.path.isfile(full_path_f) and \
+                    ((f.find(self.env['PN']) == 0 and f.find(self.env['PKGV']) != -1 and
+                      f.find(".bb") != -1) or
+                     (f.find(self.env['PN']) == 0 and f.find(".inc") != -1)):
+                with open(full_path_f + ".tmp", "w+") as temp_recipe:
+                    with open(full_path_f) as recipe:
+                        for line in recipe:
+                            if line.startswith("PR=") or line.startswith("PR ="):
+                                continue
+                            else:
+                                temp_recipe.write(line)
+                os.rename(full_path_f + ".tmp", full_path_f)
+
+        # rename recipes (not directories)
+        for path in os.listdir(self.recipe_dir):
+            full_path = os.path.join(self.recipe_dir, path)
+            if os.path.isfile(full_path) \
+              and path.find(self.env['PN']) == 0 \
+              and path.find(self.env['PKGV']) != -1:
+                new_path = re.sub(re.escape(self.env['PKGV']), self.new_ver, path)
+                self.git.mv(os.path.join(self.recipe_dir, path),
+                            os.path.join(self.recipe_dir, new_path))
+
+        # rename files/PN-PV directories to PN
+        self._rename_files_dir(self.env['PKGV'], self.new_ver)
+
+        self.recipes_renamed = True
+
+        # since we did some renaming, backup the current environment
+        self.old_env = self.env
+
+        # start formatting the commit message
+
+    def create_diff_file(self, file, old_md5, new_md5):
+        old_file = os.path.join(self.old_env['S'], file)
+        new_file = os.path.join(self.env['S'], file)
+        cmd = "diff -Nup " + old_file + " " + new_file + " > " + \
+              os.path.join(self.workdir, os.path.basename(file + ".diff"))
+
+        try:
+            stdout, stderr = bb.process.run(cmd)
+        except bb.process.ExecutionError:
+            pass
+
+        with open(os.path.join(self.workdir, "license_checksums.txt"), "w+") as f:
+            f.write("old checksum = %s\n" % old_md5)
+            f.write("new_checksum = %s\n" % new_md5)
+
+        for f in os.listdir(self.recipe_dir):
+            full_path_f = os.path.join(self.recipe_dir, f)
+            if os.path.isfile(full_path_f) and \
+                    ((f.find(self.env['PN']) == 0 and
+                      f.find(self.env['PKGV']) != -1 and
+                      f.find(".bb") != -1) or
+                     (f.find(self.env['PN']) == 0 and
+                      f.find(".inc") != -1)):
+                with open(full_path_f + ".tmp", "w+") as temp_recipe:
+                    with open(full_path_f) as recipe:
+                        for line in recipe:
+                            m = re.match("(.*)" + old_md5 + "(.*)", line)
+                            if m is not None:
+                                temp_recipe.write(m.group(1) + new_md5 + m.group(2) + "\n")
+                            else:
+                                temp_recipe.write(line)
+
+                os.rename(full_path_f + ".tmp", full_path_f)
+
+    def _change_recipe_checksums(self, fetch_log):
+        sums = {}
+
+        with open(os.path.realpath(fetch_log)) as log:
+            for line in log:
+                m = None
+                key = None
+                m1 = re.match("^SRC_URI\[(.*)md5sum\].*", line)
+                m2 = re.match("^SRC_URI\[(.*)sha256sum\].*", line)
+                if m1:
+                    m = m1
+                    key = "md5sum"
+                elif m2:
+                    m = m2
+                    key = "sha256sum"
+
+                if m:
+                    name = m.group(1)
+                    sum_line = m.group(0) + '\n'
+                    if name not in sums:
+                        sums[name] = {}
+                    sums[name][key] = sum_line;
+
+        if len(sums) == 0:
+            raise FetchError()
+
+        I(" %s: Update recipe checksums ..." % self.env['PN'])
+        # checksums are usually in the main recipe but they can also be in inc
+        # files... Go through the recipes/inc files until we find them
+        for f in os.listdir(self.recipe_dir):
+            full_path_f = os.path.join(self.recipe_dir, f)
+            if os.path.isfile(full_path_f) and \
+                    ((f.find(self.env['PN']) == 0 and f.find(self.env['PKGV']) != -1 and
+                      f.find(".bb") != -1) or
+                     (f.find(self.env['PN']) == 0 and f.find(".inc") != -1)):
+                with open(full_path_f + ".tmp", "w+") as temp_recipe:
+                    with open(full_path_f) as recipe:
+                        for line in recipe:
+                            for name in sums:
+                                m1 = re.match("^SRC_URI\["+ name + "md5sum\].*", line)
+                                m2 = re.match("^SRC_URI\["+ name + "sha256sum\].*", line)
+                                if m1:
+                                    temp_recipe.write(sums[name]["md5sum"])
+                                elif m2:
+                                    temp_recipe.write(sums[name]["sha256sum"])
+                                else:
+                                    temp_recipe.write(line)
+
+                os.rename(full_path_f + ".tmp", full_path_f)
+        
+        self.checksums_changed = True
+
+    def _is_uri_failure(self, fetch_log):
+        uri_failure = None
+        checksum_failure = None
+        with open(os.path.realpath(fetch_log)) as log:
+            for line in log:
+                if not uri_failure:
+                    uri_failure = re.match(".*Fetcher failure for URL.*", line)
+                if not checksum_failure:
+                    checksum_failure = re.match(".*Checksum mismatch.*", line)
+        if uri_failure and not checksum_failure:
+            return True
+        else:
+            return False
+
+
+    def _change_source_suffix(self, new_suffix):
+        # Will change the extension of the archive from the SRC_URI
+        for f in os.listdir(self.recipe_dir):
+            full_path_f = os.path.join(self.recipe_dir, f)
+            if os.path.isfile(full_path_f) and \
+                    ((f.find(self.env['PN']) == 0 and f.find(self.env['PKGV']) != -1 and
+                      f.find(".bb") != -1) or
+                     (f.find(self.env['PN']) == 0 and f.find(".inc") != -1)):
+                with open(full_path_f + ".tmp", "w+") as temp_recipe:
+                    with open(full_path_f) as recipe:
+                        source_found = False
+                        for line in recipe:
+                            # source on first line
+                            m1 = re.match("^SRC_URI.*\${PV}\.(.*)[\" \\\\].*", line)
+                            # SRC_URI alone on the first line
+                            m2 = re.match("^SRC_URI.*", line)
+                            # source on second line
+                            m3 = re.match(".*\${PV}\.(.*)[\" \\\\].*", line)
+                            if m1:
+                                old_suffix = m1.group(1)
+                                line = line.replace(old_suffix, new_suffix+" ")
+                            if m2 and not m1:
+                                source_found = True
+                            if m3 and source_found:
+                                old_suffix = m3.group(1)
+                                line = line.replace(old_suffix, new_suffix+" ")
+                                source_found = False
+
+                            temp_recipe.write(line)
+                os.rename(full_path_f + ".tmp", full_path_f)
+
+    def _remove_patch_uri(self, uri):
+        recipe_files = [
+            os.path.join(self.recipe_dir, self.env['PN'] + ".inc"),
+            self.env['FILE']]
+
+        for recipe_filename in recipe_files:
+            if os.path.isfile(recipe_filename):
+                with open(recipe_filename + ".tmp", "w+") as temp_recipe:
+                    with open(recipe_filename) as recipe:
+                        for line in recipe:
+                            if line.find(uri) == -1:
+                                temp_recipe.write(line)
+                                continue
+                            
+                            m1 = re.match("SRC_URI *\+*= *\" *" + uri + " *\"", line)
+                            m2 = re.match("(SRC_URI *\+*= *\" *)" + uri + " *\\\\", line)
+                            m3 = re.match("[\t ]*" + uri + " *\\\\", line)
+                            m4 = re.match("([\t ]*)" + uri + " *\"", line)
+
+                            # patch on a single SRC_URI line:
+                            if m1:
+                                continue
+                            # patch is on the first SRC_URI line
+                            elif m2:
+                                temp_recipe.write(m2.group(1) + "\\\n")
+                            # patch is in the middle
+                            elif m3:
+                                continue
+                            # patch is last in list
+                            elif m4:
+                                temp_recipe.write(m4.group(1) + "\"\n")
+                            # nothing matched in recipe but we deleted the patch
+                            # anyway? Then we must bail out!
+                            else:
+                                return False
+
+                os.rename(recipe_filename + ".tmp", recipe_filename)
+
+        return True
+
+    def _remove_faulty_patch(self, patch_log):
+        patch_file = None
+        is_reverse_applied = False
+
+        with open(patch_log) as log:
+            for line in log:
+                m1 = re.match("^Patch ([^ ]*) does not apply.*", line)
+                m2 = re.match("Patch ([^ ]*) can be reverse-applied", line)
+                if m2:
+                    m1 = m2
+                    is_reverse_applied = True
+                if m1:
+                    patch_file = m1.group(1)
+                    break
+
+        if not patch_file:
+            return False
+
+        I(" %s: Removing patch %s ..." % (self.env['PN'], patch_file))
+        reason = None
+        found = False
+        dirs = [self.env['PN'] + "-" + self.env['PKGV'], self.env['PN'], "files"]
+        for dir in dirs:
+            patch_file_path = os.path.join(self.recipe_dir, dir, patch_file)
+            if not os.path.exists(patch_file_path):
+                continue
+            else:
+                found = True
+                # Find out upstream status of the patch
+                with open(patch_file_path) as patch:
+                    for line in patch:
+                        m = re.match(".*Upstream-Status:(.*)\n", line)
+                        if m:
+                            reason = m.group(1).strip().split()[0].lower()
+                os.remove(patch_file_path)
+                if not self._remove_patch_uri("file://" + patch_file):
+                    return False
+        if not found:
+            return False
+
+        self.rm_patches_msg += " * " + patch_file
+        if reason:
+            self.rm_patches_msg += " (" + reason + ") "
+        if is_reverse_applied:
+            self.rm_patches_msg += "+ reverse-applied"
+        self.rm_patches_msg += "\n"
+        return True
+
+    def _is_license_issue(self, config_log):
+        with open(config_log) as log:
+            for line in log:
+                m = re.match("ERROR: " + self.env['PN'] +
+                             "[^:]*: md5 data is not matching for file", line)
+                if m is not None:
+                    return True
+
+        return False
+
+    def _license_issue_handled(self, config_log):
+        license_file = None
+        with open(config_log) as log:
+            for line in log:
+                if not line.startswith("ERROR:"):
+                    continue
+                m_old = re.match("ERROR: " + self.env['PN'] +
+                        "[^:]*: md5 data is not matching for file://([^;]*);md5=(.*)$", line)
+                if not m_old:
+                    m_old = re.match("ERROR: " + self.env['PN'] +
+                            "[^:]*: md5 data is not matching for file://([^;]*);beginline=[0-9]*;endline=[0-9]*;md5=(.*)$", line)
+                if not m_old:
+                    m_old = re.match("ERROR: " + self.env['PN'] +
+                            "[^:]*: md5 data is not matching for file://([^;]*);endline=[0-9]*;md5=(.*)$", line)
+                if not m_old:
+                    m_old = re.match("ERROR: " + self.env['PN'] +
+                            "[^:]*: md5 data is not matching for file://([^;]*);beginline=[0-9]*;md5=(.*)$", line)
+                m_new = re.match("ERROR: " + self.env['PN'] +
+                        "[^:]*: The new md5 checksum is (.*)", line)
+                if m_old:
+                    license_file = m_old.group(1)
+                    old_md5 = m_old.group(2)
+                elif m_new:
+                    new_md5 = m_new.group(1)
+        
+        if license_file is not None:
+            self.create_diff_file(license_file, old_md5, new_md5)
+            self.license_diff_file = os.path.join(self.workdir, os.path.basename(license_file + ".diff"))
+            if self.interactive:
+                W("  %s: license checksum failed for file %s. The recipe has"
+                  "been updated! View diff? (Y/n)" % (self.env['PN'], license_file))
+                answer = sys.stdin.readline().strip().upper()
+                if answer == '' or answer == 'Y':
+                    I(" ################ Licence file diff #################")
+                    with open(self.license_diff_file) as diff:
+                        I("%s" % diff.read())
+                    I(" ####################################################")
+                I(" Retry compilation? (Y/n)")
+                answer = sys.stdin.readline().strip().upper()
+                if answer == '' or answer == 'Y':
+                    return True
+            else:
+                W(" %s: license checksum failed for file %s."
+                  " The recipe has been updated! Diff file located at %s" %
+                  (self.env['PN'], license_file, self.license_diff_file))
+                I(" Recompiling ...")
+                self.commit_msg += "License checksum changed for file " + license_file
+                return True
+
+        return False
+
+    def get_license_diff_file_name(self):
+        file_name = None
+        if not self.license_diff_file is None:
+            file_name = os.path.basename(self.license_diff_file)
+
+        return file_name
+
+    def _get_failed_recipes(self, output):
+        failed_tasks = dict()
+        machine = None
+
+        for line in output.split("\n"):
+            machine_match = re.match("MACHINE[\t ]+= *\"(.*)\"$", line)
+            task_log_match = re.match("ERROR: Logfile of failure stored in: (.*/([^/]*)/[^/]*/temp/log\.(.*)\.[0-9]*)", line)
+            # For some reason do_package is reported differently
+            qa_issue_match = re.match("ERROR: QA Issue: ([^ :]*): (.*) not shipped", line)
+
+            if task_log_match:
+                failed_tasks[task_log_match.group(2)] = (task_log_match.group(3), task_log_match.group(1))
+            elif qa_issue_match:
+                # Improvise path to log file
+                failed_tasks[qa_issue_match.group(1)] = ("do_package", self.bb.get_stdout_log())
+            elif machine_match:
+                machine = machine_match.group(1)
+
+        # we didn't detect any failed tasks? then something else is wrong
+        if len(failed_tasks) == 0:
+            raise Error("could not detect failed task")
+
+        return (machine, failed_tasks)
+
+    def _is_incompatible_host(self, output):
+        for line in output.split("\n"):
+            incomp_host = re.match("ERROR: " + self.env['PN'] + " was skipped: incompatible with host (.*) \(.*$", line)
+
+            if incomp_host is not None:
+                return True
+
+        return False
+
+    def _add_not_shipped(self, package_log):
+        files_not_shipped = False
+        files = []
+        occurences = []
+        prefixes = {
+          "/usr"            : "prefix",
+          "/bin"            : "base_bindir",
+          "/sbin"           : "base_sbindir",
+          "/lib"            : "base_libdir",
+          "/usr/share"      : "datadir",
+          "/etc"            : "sysconfdir",
+          "/var"            : "localstatedir",
+          "/usr/share/info" : "infodir",
+          "/usr/share/man"  : "mandir",
+          "/usr/share/doc"  : "docdir",
+          "/srv"            : "servicedir",
+          "/usr/bin"        : "bindir",
+          "/usr/sbin"       : "sbindir",
+          "/usr/libexec"    : "libexecdir",
+          "/usr/lib"        : "libdir",
+          "/usr/include"    : "includedir",
+          "/usr/lib/opie"   : "palmtopdir",
+          "/usr/lib/opie"   : "palmqtdir",
+        }
+
+        I(" %s: Add new files in recipe ..." %  self.env['PN'])
+        with open(package_log) as log:
+            for line in log:
+                if re.match(".*Files/directories were installed but not shipped.*", line):
+                    files_not_shipped = True
+                # Extract path
+                line = line.strip()
+                if line:
+                    line = line.split()[0]
+                if files_not_shipped and os.path.isabs(line):
+                    # Count occurences for globbing
+                    path_exists = False
+                    for i in range(0, len(files)):
+                        if line.find(files[i]) == 0:
+                            path_exists = True
+                            occurences[i] += 1
+                            break
+                    if not path_exists:
+                        files.append(line)
+                        occurences.append(1)
+
+        for i in range(0, len(files)):
+            # Change paths to globbing expressions where is the case
+            if occurences[i] > 1:
+                files[i] += "/*"
+            largest_prefix = ""
+            # Substitute prefix
+            for prefix in prefixes:
+                if files[i].find(prefix) == 0 and len(prefix) > len(largest_prefix):
+                    largest_prefix = prefix
+            if largest_prefix:
+                replacement = "${" + prefixes[largest_prefix] + "}"
+                files[i] = files[i].replace(largest_prefix, replacement)
+
+        recipe_files = [
+            os.path.join(self.recipe_dir, self.env['PN'] + ".inc"),
+            self.env['FILE']]
+
+        # Append the new files
+        for recipe_filename in recipe_files:
+            if os.path.isfile(recipe_filename):
+                with open(recipe_filename + ".tmp", "w+") as temp_recipe:
+                    with open(recipe_filename) as recipe:
+                        files_clause = False
+                        for line in recipe:
+                            if re.match("^FILES_\${PN}[ +=].*", line):
+                                files_clause = True
+                                temp_recipe.write(line)
+                                continue
+                            # Get front spacing
+                            if files_clause:
+                                front_spacing = re.sub("[^ \t]", "", line)
+                            # Append once the last line has of FILES has been reached
+                            if re.match(".*\".*", line) and files_clause:
+                                files_clause = False
+                                line = line.replace("\"", "")
+                                line = line.rstrip()
+                                front_spacing = re.sub("[^ \t]", "", line)
+                                # Do not write an empty line
+                                if line.strip():
+                                    temp_recipe.write(line + " \\\n")
+                                # Add spacing in case there was none
+                                if len(front_spacing) == 0:
+                                    front_spacing = " " * 8
+                                # Write to file
+                                for i in range(len(files)-1):
+                                    line = front_spacing + files[i] + " \\\n"
+                                    temp_recipe.write(line)
+
+                                line = front_spacing + files[len(files) - 1] + "\"\n"
+                                temp_recipe.write(line)
+                                continue
+
+                            temp_recipe.write(line)
+
+                os.rename(recipe_filename + ".tmp", recipe_filename)
+
+    def unpack(self):
+        self.bb.unpack(self.env['PN'])
+
+    def fetch(self):
+        from recipe.git import GitRecipe
+
+        def _try_fetch():
+            try:
+                self.bb.fetch(self.env['PN'])
+                return
+            except Error as e:
+                machine, failed_recipes = self._get_failed_recipes(e.stdout)
+                if not self.env['PN'] in failed_recipes:
+                    raise Error("Unknown error occured during fetch",
+                            stdout = e.stdout, stderr = e.stderr)
+
+                fetch_log = failed_recipes[self.env['PN']][1]
+
+                if not self._is_uri_failure(fetch_log) and not \
+                        self.checksums_changed:
+                    self._change_recipe_checksums(fetch_log)
+                    self.checksums_changed = True
+                    return True
+
+                return False
+
+        succeed = _try_fetch()
+
+        if not succeed and not isinstance(self, GitRecipe):
+            for sfx in self.suffixes:
+                I(" Trying new SRC_URI suffix: %s ..." % sfx)
+                self._change_source_suffix(sfx)
+
+                succeed = _try_fetch()
+                if succeed:
+                    break
+
+        if not succeed:
+            raise Error("Can't built a valid SRC_URI")
+        elif self.recipes_renamed and not self.checksums_changed:
+            raise Error("Fetch succeeded without changing checksums")
+
+    def cleanall(self):
+        self.bb.cleanall(self.env['PN'])
+
+    def _clean_failed_recipes(self, failed_recipes):
+        already_retried = False
+        for recipe in failed_recipes:
+            if recipe in self.retried_recipes:
+                # we already retried, we'd best leave it to a human to handle
+                # it :)
+                already_retried = True
+            # put the recipe in the retried list
+            self.retried_recipes.add(recipe)
+
+        if already_retried:
+            return False
+        else:
+            I(" %s: The following recipe(s): %s, failed.  "
+              "Doing a 'cleansstate' and then retry ..." %
+              (self.env['PN'], ' '.join(failed_recipes.keys())))
+
+            self.bb.cleansstate(' '.join(failed_recipes.keys()))
+            return True
+
+    def _undo_temporary(self):
+        # Undo removed patches
+        if self.removed_patches:
+            self.git.checkout_branch("upgrades")
+            self.git.delete_branch("remove_patches")
+            self.git.reset_hard()
+            self.git.reset_soft(1)
+            self.removed_patches = False
+
+    def compile(self, machine):
+        try:
+            self.bb.complete(self.env['PN'], machine)
+            if self.removed_patches:
+                # move temporary changes into upgrades branch
+                self.git.checkout_branch("upgrades")
+                self.git.delete_branch("remove_patches")
+                self.git.reset_soft(1)
+                self.commit_msg += self.rm_patches_msg + "\n"
+                self.removed_patches = False
+        except Error as e:
+            if self._is_incompatible_host(e.stdout):
+                W(" %s: compilation failed: incompatible host" % self.env['PN'])
+                return
+            machine, failed_recipes = self._get_failed_recipes(e.stdout)
+            if not self.env['PN'] in failed_recipes:
+                if not self._clean_failed_recipes(failed_recipes):
+                    self._undo_temporary()
+                    raise CompilationError()
+
+                # retry
+                self.compile(machine)
+            else:
+                failed_task = failed_recipes[self.env['PN']][0]
+                log_file = failed_recipes[self.env['PN']][1]
+                if failed_task == "do_patch":
+                    # Remove one patch after the other until
+                    # compilation works.
+                    if not self.removed_patches:
+                        self.git.commit("temporary")
+                        self.git.create_branch("remove_patches")
+                        self.git.checkout_branch("remove_patches")
+                        self.removed_patches = True
+                    if not self._remove_faulty_patch(log_file):
+                        self._undo_temporary()
+                        raise PatchError()
+
+                    # retry
+                    I(" %s: Recompiling for %s ..." % (self.env['PN'], machine))
+                    self.compile(machine)
+                elif failed_task == "do_configure":
+                    self._undo_temporary()
+                    if not self._is_license_issue(log_file):
+                        raise ConfigureError()
+
+                    if not self._license_issue_handled(log_file):
+                        raise LicenseError()
+                    #retry
+                    self.compile(machine)
+                elif failed_task == "do_fetch":
+                    raise FetchError()
+                elif failed_task == "do_package":
+                    self._add_not_shipped(log_file)
+                    self.compile(machine)
+                else:
+                    self._undo_temporary()
+                    # throw a compilation exception for everything else. It
+                    # doesn't really matter
+                    raise CompilationError()
diff --git a/modules/recipe/git.py b/modules/recipe/git.py
new file mode 100644
index 0000000..7526a67
--- /dev/null
+++ b/modules/recipe/git.py
@@ -0,0 +1,94 @@
+#!/usr/bin/env python
+# vim: set ts=4 sw=4 et:
+#
+# Copyright (c) 2013 - 2014 Intel Corporation
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+#
+# AUTHORS
+# Laurentiu Palcu   <laurentiu.palcu at intel.com>
+# Marius Avram      <marius.avram at intel.com>
+#
+
+from recipe.base import Recipe
+
+class GitRecipe(Recipe):
+    def _extract_tag_from_ver(self, ver):
+        m = re.match("(.*)\+.*\+.*", ver)
+        if m is not None:
+            return m.group(1)
+
+        # allow errors in the reporting system
+        return ver
+
+    def _get_tag_sha1(self, new_tag):
+        m = re.match(".*(git://[^ ;]*).*", self.env['SRC_URI'])
+        if m is None:
+            raise Error("could not extract repo url from SRC_URI")
+
+        repo_url = m.group(1)
+        tags = self.git.ls_remote(repo_url, "--tags")
+
+        # Try to find tag ending with ^{}
+        for tag in tags.split('\n'):
+            if tag.endswith(new_tag + "^{}"):
+                return tag.split()[0]
+
+        # If not found, try to find simple tag
+        for tag in tags.split('\n'):
+            if tag.endswith(new_tag):
+                return tag.split()[0]
+
+        return None
+
+    def rename(self):
+        old_git_tag = self._extract_tag_from_ver(self.env['PKGV'])
+        new_git_tag = self._extract_tag_from_ver(self.new_ver)
+
+        if new_git_tag == old_git_tag:
+            raise UpgradeNotNeededError()
+
+        tag_sha1 = self._get_tag_sha1(new_git_tag)
+        if tag_sha1 is None:
+            raise Error("could not extract tag sha1")
+
+        for f in os.listdir(self.recipe_dir):
+            full_path_f = os.path.join(self.recipe_dir, f)
+            if os.path.isfile(full_path_f) and \
+                    ((f.find(self.env['PN']) == 0 and (f.find(old_git_tag) != -1 or
+                      f.find("git") != -1) and f.find(".bb") != -1) or
+                     (f.find(self.env['PN']) == 0 and f.find(".inc") != -1)):
+                with open(full_path_f + ".tmp", "w+") as temp_recipe:
+                    with open(full_path_f) as recipe:
+                        for line in recipe:
+                            m1 = re.match("^SRCREV *= *\".*\"", line)
+                            m2 = re.match("PV *= *\"[^\+]*(.*)\"", line)
+                            if m1 is not None:
+                                temp_recipe.write("SRCREV = \"" + tag_sha1 + "\"\n")
+                            elif m2 is not None:
+                                temp_recipe.write("PV = \"" + new_git_tag + m2.group(1) + "\"\n")
+                            else:
+                                temp_recipe.write(line)
+
+                os.rename(full_path_f + ".tmp", full_path_f)
+
+        self.env['PKGV'] = old_git_tag
+        self.new_ver = new_git_tag
+
+        super(GitRecipe, self).rename()
+
+    def fetch(self):
+        pass
+
diff --git a/modules/recipe/svn.py b/modules/recipe/svn.py
new file mode 100644
index 0000000..6d60529
--- /dev/null
+++ b/modules/recipe/svn.py
@@ -0,0 +1,28 @@
+#!/usr/bin/env python
+# vim: set ts=4 sw=4 et:
+#
+# Copyright (c) 2013 - 2014 Intel Corporation
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+#
+# AUTHORS
+# Laurentiu Palcu   <laurentiu.palcu at intel.com>
+# Marius Avram      <marius.avram at intel.com>
+#
+
+from base import Recipe
+
+class SvnRecipe(Recipe):
+    pass
diff --git a/modules/statistics.py b/modules/statistics.py
new file mode 100644
index 0000000..32a6748
--- /dev/null
+++ b/modules/statistics.py
@@ -0,0 +1,102 @@
+#!/usr/bin/env python
+# vim: set ts=4 sw=4 et:
+#
+# Copyright (c) 2013 - 2014 Intel Corporation
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+#
+# AUTHORS
+# Laurentiu Palcu   <laurentiu.palcu at intel.com>
+# Marius Avram      <marius.avram at intel.com>
+#
+
+class Statistics(object):
+    def __init__(self):
+        self.succeeded = dict()
+        self.failed = dict()
+        self.succeeded["total"] = 0
+        self.failed["total"] = 0
+        self.upgrade_stats = dict()
+        self.maintainers = set()
+        self.total_attempted = 0
+
+    def update(self, pn, new_ver, maintainer, error):
+        if type(error).__name__ == "UpgradeNotNeededError":
+            return
+        elif error is None:
+            status = "Succeeded"
+        else:
+            status = str(error)
+
+        if not status in self.upgrade_stats:
+            self.upgrade_stats[status] = []
+
+        self.upgrade_stats[status].append((pn, new_ver, maintainer))
+
+        # add maintainer to the set of unique maintainers
+        self.maintainers.add(maintainer)
+
+        if not maintainer in self.succeeded:
+            self.succeeded[maintainer] = 0
+        if not maintainer in self.failed:
+            self.failed[maintainer] = 0
+
+        if status == "Succeeded":
+            self.succeeded["total"] += 1
+            self.succeeded[maintainer] += 1
+        else:
+            self.failed["total"] += 1
+            self.failed[maintainer] += 1
+
+        self.total_attempted += 1
+
+    def pkg_stats(self):
+        stat_msg = "\nUpgrade statistics:\n"
+        stat_msg += "====================================================\n"
+        for status in self.upgrade_stats:
+            list_len = len(self.upgrade_stats[status])
+            if list_len > 0:
+                stat_msg += "* " + status + ": " + str(list_len) + "\n"
+
+                for pkg, new_ver, maintainer in self.upgrade_stats[status]:
+                    stat_msg += "    " + pkg + ", " + new_ver + ", " + \
+                                maintainer + "\n"
+
+        if self.total_attempted == 0:
+            percent_succeded = 0
+            percent_failed = 0
+        else:
+            percent_succeded = self.succeeded["total"] * 100.0 / self.total_attempted
+            percent_failed = self.failed["total"] * 100.0 / self.total_attempted
+        stat_msg += "++++++++++++++++++++++++++++++++++++++++++++++++++++\n"
+        stat_msg += "TOTAL: attempted=%d succeeded=%d(%.2f%%) failed=%d(%.2f%%)\n\n" % \
+                    (self.total_attempted, self.succeeded["total"],
+                    percent_succeded,
+                    self.failed["total"],
+                    percent_failed)
+
+        return stat_msg
+
+    def maintainer_stats(self):
+        stat_msg = "* Statistics per maintainer:\n"
+        for m in self.maintainers:
+            attempted = self.succeeded[m] + self.failed[m]
+            stat_msg += "    %s: attempted=%d succeeded=%d(%.2f%%) failed=%d(%.2f%%)\n\n" % \
+                        (m.split("@")[0], attempted, self.succeeded[m],
+                        self.succeeded[m] * 100.0 / attempted,
+                        self.failed[m],
+                        self.failed[m] * 100.0 / attempted)
+
+        return stat_msg
diff --git a/modules/steps.py b/modules/steps.py
new file mode 100644
index 0000000..b85a0dc
--- /dev/null
+++ b/modules/steps.py
@@ -0,0 +1,143 @@
+#!/usr/bin/env python
+# vim: set ts=4 sw=4 et:
+#
+# Copyright (c) 2013 - 2015 Intel Corporation
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+#
+
+import os
+import sys
+
+from logging import debug as D
+from logging import info as I
+from logging import warning as W
+from logging import error as E
+from logging import critical as C
+
+from errors import *
+from buildhistory import BuildHistory
+
+from recipe.base import Recipe
+from recipe.git import GitRecipe
+from recipe.svn import SvnRecipe
+
+def load_env(bb, git, opts, pkg_ctx):
+    stdout = git.status()
+    if stdout != "":
+        if opts['interactive']:
+            W(" %s: git repository has uncommited work which will be dropped!" \
+                    " Proceed? (y/N)" % pkg_ctx['PN'])
+            answer = sys.stdin.readline().strip().upper()
+            if answer == '' or answer != 'Y':
+                I(" %s: User abort!" % pkg_ctx['PN'])
+                exit(1)
+
+        I(" %s: Dropping uncommited work!" % pkg_ctx['PN'])
+        git.reset_hard()
+        git.clean_untracked()
+
+    pkg_ctx['env'] = bb.env(pkg_ctx['PN'])
+    if pkg_ctx['env']['PV'] == pkg_ctx['NPV']:
+        raise UpgradeNotNeededError
+
+def load_dirs(bb, git, opts, pkg_ctx):
+    pkg_ctx['workdir'] = os.path.join(pkg_ctx['base_dir'], pkg_ctx['PN'])
+    os.mkdir(pkg_ctx['workdir'])
+
+    pkg_ctx['recipe_dir'] = os.path.dirname(pkg_ctx['env']['FILE'])
+
+def clean_repo(bb, git, opts, pkg_ctx):
+    try:
+        git.checkout_branch("upgrades")
+    except Error:
+        git.create_branch("upgrades")
+
+    try:
+        git.delete_branch("remove_patches")
+    except:
+        pass
+
+def detect_recipe_type(bb, git, opts, pkg_ctx):
+    if pkg_ctx['env']['SRC_URI'].find("ftp://") != -1 or  \
+            pkg_ctx['env']['SRC_URI'].find("http://") != -1 or \
+            pkg_ctx['env']['SRC_URI'].find("https://") != -1:
+        recipe = Recipe
+    elif pkg_ctx['env']['SRC_URI'].find("git://") != -1:
+        recipe = GitRecipe
+    else:
+        raise UnsupportedProtocolError
+
+    pkg_ctx['recipe'] = recipe(pkg_ctx['env'], pkg_ctx['NPV'],
+            opts['interactive'], pkg_ctx['workdir'],
+            pkg_ctx['recipe_dir'], bb, git)
+
+def buildhistory_init(bb, git, opts, pkg_ctx):
+    if not opts['buildhistory_enabled']:
+        return
+
+    pkg_ctx['buildhistory'] = BuildHistory(bb, pkg_ctx['PN'],
+            pkg_ctx['workdir'])
+    I(" %s: Initial buildhistory for %s ..." % (pkg_ctx['PN'],
+            opts['machines']))
+    pkg_ctx['buildhistory'].init(opts['machines'])
+
+def unpack_original(bb, git, opts, pkg_ctx):
+    pkg_ctx['recipe'].unpack()
+
+def rename(bb, git, opts, pkg_ctx):
+    pkg_ctx['recipe'].rename()
+
+    pkg_ctx['env'] = bb.env(pkg_ctx['PN'])
+
+    pkg_ctx['recipe'].update_env(pkg_ctx['env'])
+
+def cleanall(bb, git, opts, pkg_ctx):
+    pkg_ctx['recipe'].cleanall()
+
+def fetch(bb, git, opts, pkg_ctx):
+    pkg_ctx['recipe'].fetch()
+
+def compile(bb, git, opts, pkg_ctx):
+    if opts['skip_compilation']:
+        W(" %s: Compilation was skipped by user choice!")
+        return
+
+    for machine in opts['machines']:
+        I(" %s: compiling for %s ..." % (pkg_ctx['PN'], machine))
+        pkg_ctx['recipe'].compile(machine)
+        if opts['buildhistory_enabled']:
+            pkg_ctx['buildhistory'].add()
+
+def buildhistory_diff(bb, git, opts, pkg_ctx):
+    if not opts['buildhistory_enabled']:
+        return
+
+    I(" %s: Checking buildhistory ..." % pkg_ctx['PN'])
+    pkg_ctx['buildhistory'].diff()
+
+upgrade_steps = [
+    (load_env, "Loading environment ..."),
+    (load_dirs, None),
+    (clean_repo, "Cleaning git repository of temporary branch ..."),
+    (detect_recipe_type, None),
+    (buildhistory_init, None),
+    (unpack_original, "Fetch & unpack original version ..."),
+    (rename, "Renaming recipes, reset PR (if exists) ..."),
+    (cleanall, "Clean all ..."),
+    (fetch, "Fetch new version (old checksums) ..."),
+    (compile, None),
+    (buildhistory_diff, None)
+]
diff --git a/modules/utils/__init__.py b/modules/utils/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/modules/utils/bitbake.py b/modules/utils/bitbake.py
new file mode 100644
index 0000000..cdbce2b
--- /dev/null
+++ b/modules/utils/bitbake.py
@@ -0,0 +1,124 @@
+#!/usr/bin/env python
+# vim: set ts=4 sw=4 et:
+#
+# Copyright (c) 2013 - 2014 Intel Corporation
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+#
+# AUTHORS
+# Laurentiu Palcu   <laurentiu.palcu at intel.com>
+# Marius Avram      <marius.avram at intel.com>
+#
+
+import os
+import logging as log
+from logging import info as I
+from logging import debug as D
+from logging import error as E
+from logging import critical as C
+import sys
+import re
+
+from errors import *
+
+for path in os.environ["PATH"].split(':'):
+    if os.path.exists(path) and "bitbake" in os.listdir(path):
+        sys.path.insert(0, os.path.join(path, "../lib"))
+        import bb
+
+BITBAKE_ERROR_LOG = 'bitbake_error_log.txt'
+
+class Bitbake(object):
+    def __init__(self, build_dir):
+        self.build_dir = build_dir
+        self.log_dir = None
+        super(Bitbake, self).__init__()
+
+    def _cmd(self, recipe=None, options=None, env_var=None, output_filter=None):
+        cmd = ""
+        if env_var is not None:
+            cmd += env_var + " "
+        cmd += "bitbake "
+        if options is not None:
+            cmd += options + " "
+
+        if recipe is not None:
+            cmd += recipe
+
+        if output_filter is not None:
+            cmd += ' |  grep ' + output_filter
+
+        os.chdir(self.build_dir)
+
+        try:
+            stdout, stderr = bb.process.run(cmd)
+        except bb.process.ExecutionError as e:
+            D("%s returned:\n%s" % (cmd, e.__str__()))
+
+            if self.log_dir is not None and os.path.exists(self.log_dir):
+                with open(os.path.join(self.log_dir, BITBAKE_ERROR_LOG), "w+") as log:
+                    log.write(e.stdout)
+
+            raise Error("\'" + cmd + "\' failed", e.stdout, e.stderr)
+
+        return stdout
+
+    def set_log_dir(self, dir):
+        self.log_dir = dir
+
+    def get_stdout_log(self):
+        return os.path.join(self.log_dir, BITBAKE_ERROR_LOG)
+
+    def env(self, recipe=None):
+        stdout = self._cmd(recipe, "-e", output_filter="-v \"^#\"")
+
+        assignment = re.compile("^([^ \t=]*)=(.*)")
+        bb_env = dict()
+        for line in stdout.split('\n'):
+            m = assignment.match(line)
+            if m:
+                if m.group(1) in bb_env:
+                    continue
+
+                bb_env[m.group(1)] = m.group(2).strip("\"")
+
+        if not bb_env:
+            raise EmptyEnvError(stdout)
+
+        return bb_env
+
+    def fetch(self, recipe):
+        return self._cmd(recipe, "-c fetch")
+
+    def unpack(self, recipe):
+        return self._cmd(recipe, "-c unpack")
+
+    def checkpkg(self, recipe):
+        if recipe == "universe":
+            return self._cmd(recipe, "-c checkpkg -k")
+        else:
+            return self._cmd(recipe, "-c checkpkg")
+
+    def cleanall(self, recipe):
+        return self._cmd(recipe, "-c cleanall")
+
+    def cleansstate(self, recipe):
+        return self._cmd(recipe, "-c cleansstate")
+
+    def complete(self, recipe, machine):
+        return self._cmd(recipe, env_var="MACHINE=" + machine)
+
+    def dependency_graph(self, package_list):
+        return self._cmd(package_list, "-g")
diff --git a/modules/utils/emailhandler.py b/modules/utils/emailhandler.py
new file mode 100644
index 0000000..970fb59
--- /dev/null
+++ b/modules/utils/emailhandler.py
@@ -0,0 +1,108 @@
+#!/usr/bin/env python
+# vim: set ts=4 sw=4 et:
+#
+# Copyright (c) 2013 - 2014 Intel Corporation
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+#
+# AUTHORS
+# Laurentiu Palcu   <laurentiu.palcu at intel.com>
+# Marius Avram      <marius.avram at intel.com>
+#
+
+import os
+import logging as log
+from logging import error as E
+from logging import info as I
+from smtplib import SMTP
+import mimetypes
+from email.mime.text import MIMEText
+from email.mime.base import MIMEBase
+from email.mime.multipart import MIMEMultipart
+from email.generator import Generator
+import shutil
+from cStringIO import StringIO
+
+class Email(object):
+    def __init__(self, settings):
+        self.smtp_host = None
+        self.smtp_port = None
+        self.from_addr = None
+        if "smtp" in settings:
+            smtp_entry = settings["smtp"].split(":")
+            if len(smtp_entry) == 1:
+                self.smtp_host = smtp_entry[0]
+                self.smtp_port = 25
+            elif len(smtp_entry) == 2:
+                self.smtp_host = smtp_entry[0]
+                self.smtp_port = smtp_entry[1]
+        else:
+            E(" smtp host not set! Sending emails disabled!")
+
+        if "from" in settings:
+            self.from_addr = settings["from"]
+        else:
+            E(" 'From' address not set! Sending emails disabled!")
+
+        super(Email, self).__init__()
+
+    def send_email(self, to_addr, subject, text, files=[], cc_addr=None):
+        if self.smtp_host is None or self.from_addr is None:
+            return 0
+
+        I(" Sending email to: %s" % to_addr)
+
+        msg = MIMEMultipart()
+        msg['From'] = self.from_addr
+        if type(to_addr) is list:
+            msg['To'] = ', '.join(to_addr)
+        else:
+            msg['To'] = to_addr
+        if cc_addr is not None:
+            if type(cc_addr) is list:
+                msg['Cc'] = ', '.join(cc_addr)
+            else:
+                msg['Cc'] = cc_addr
+        msg['Subject'] = subject
+
+        msg.attach(MIMEText(text))
+
+        for file in files:
+            ctype, encoding = mimetypes.guess_type(file)
+            if ctype is None or encoding is not None:
+                ctype = 'application/octet-stream'
+            maintype, subtype = ctype.split('/', 1)
+
+            if maintype == "text":
+                attachment = MIMEText(open(file).read(), _subtype=subtype)
+            else:
+                attachment = MIMEBase(maintype, _subtype=subtype)
+                attachment.set_payload(open(file, 'rb').read())
+
+            attachment.add_header('Content-Disposition', 'attachment; filename="%s"'
+                                  % os.path.basename(file))
+            msg.attach(attachment)
+
+        out = StringIO()
+        Generator(out, mangle_from_=False).flatten(msg)
+        msg_text = out.getvalue()
+
+        try:
+            smtp = SMTP(self.smtp_host, self.smtp_port)
+            smtp.sendmail(self.from_addr, to_addr, msg_text)
+            smtp.close()
+        except Exception as e:
+            E("Could not send email: %s" % str(e))
+
diff --git a/modules/utils/git.py b/modules/utils/git.py
new file mode 100644
index 0000000..48ed46d
--- /dev/null
+++ b/modules/utils/git.py
@@ -0,0 +1,102 @@
+#!/usr/bin/env python
+# vim: set ts=4 sw=4 et:
+#
+# Copyright (c) 2013 - 2014 Intel Corporation
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+#
+# AUTHORS
+# Laurentiu Palcu   <laurentiu.palcu at intel.com>
+# Marius Avram      <marius.avram at intel.com>
+#
+
+import os
+import logging as log
+from logging import debug as D
+
+from utils.bitbake import *
+
+class Git(object):
+    def __init__(self, dir):
+        self.repo_dir = dir
+        super(Git, self).__init__()
+
+    def _cmd(self, operation):
+        os.chdir(self.repo_dir)
+
+        cmd = "git " + operation
+        try:
+            stdout, stderr = bb.process.run(cmd)
+        except bb.process.ExecutionError as e:
+            D("%s returned:\n%s" % (cmd, e.__str__()))
+            raise Error("The following git command failed: " + operation,
+                        e.stdout, e.stderr)
+
+        return stdout
+
+    def mv(self, src, dest):
+        return self._cmd("mv -f " + src + " " + dest)
+
+    def stash(self):
+        return self._cmd("stash")
+
+    def commit(self, commit_message, author=None):
+        if author is None:
+            return self._cmd("commit -a -s -m \"" + commit_message + "\"")
+        else:
+            return self._cmd("commit -a --author=\"" + author + "\" -m \"" + commit_message + "\"")
+
+    def create_patch(self, out_dir):
+        return self._cmd("format-patch -M10 -1 -o " + out_dir)
+
+    def status(self):
+        return self._cmd("status --porcelain")
+
+    def checkout_branch(self, branch_name):
+        return self._cmd("checkout " + branch_name)
+
+    def create_branch(self, branch_name):
+        return self._cmd("checkout -b " + branch_name)
+
+    def delete_branch(self, branch_name):
+        return self._cmd("branch -D " + branch_name)
+
+    def pull(self):
+        return self._cmd("pull")
+
+    def reset_hard(self, no_of_patches=0):
+        if no_of_patches == 0:
+            return self._cmd("reset --hard HEAD")
+        else:
+            return self._cmd("reset --hard HEAD~" + str(no_of_patches))
+
+    def reset_soft(self, no_of_patches):
+        return self._cmd("reset --soft HEAD~" + str(no_of_patches))
+
+    def clean_untracked(self):
+        return self._cmd("clean -fd")
+
+    def last_commit(self, branch_name):
+        return self._cmd("log --pretty=format:\"%H\" -1 " + branch_name)
+
+    def ls_remote(self, repo_url=None, options=None, refs=None):
+        cmd = "ls-remote"
+        if options is not None:
+            cmd += " " + options
+        if repo_url is not None:
+            cmd += " " + repo_url
+        if refs is not None:
+            cmd += " " + refs
+        return self._cmd(cmd)
diff --git a/recipe.py b/recipe.py
deleted file mode 100644
index 64e6e14..0000000
--- a/recipe.py
+++ /dev/null
@@ -1,669 +0,0 @@
-#!/usr/bin/env python
-# vim: set ts=4 sw=4 et:
-#
-# Copyright (c) 2013 - 2014 Intel Corporation
-#
-# This program is free software; you can redistribute it and/or
-# modify it under the terms of the GNU General Public License
-# as published by the Free Software Foundation; either version 2
-# of the License, or (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
-#
-# AUTHORS
-# Laurentiu Palcu   <laurentiu.palcu at intel.com>
-# Marius Avram      <marius.avram at intel.com>
-#
-
-import os
-import re
-import sys
-import logging as log
-from logging import debug as D
-from logging import info as I
-from logging import warning as W
-from errors import *
-from bitbake import *
-
-class Recipe(object):
-    def __init__(self, env, new_ver, interactive, workdir, recipe_dir, bitbake, git):
-        self.env = env
-        self.new_ver = new_ver
-        self.interactive = interactive
-        self.workdir = workdir
-        self.recipe_dir = recipe_dir
-        self.bb = bitbake
-        self.bb.set_log_dir(workdir)
-        self.git = git
-
-        self.retried_recipes = set()
-        self.license_diff_file = None
-
-        self.recipes_renamed = False
-        self.checksums_changed = False
-
-        self.removed_patches = False
-
-        self.suffixes = [
-            "tar.gz", "tgz", "zip", "tar.bz2", "tar.xz", "tar.lz4", "bz2",
-            "lz4", "orig.tar.gz", "src.tar.gz", "src.rpm", "src.tgz",
-            "svnr\d+.tar.bz2", "stable.tar.gz", "src.rpm"]
-        self.old_env = None
-
-        self.commit_msg = self.env['PN'] + ": upgrade to " + self.new_ver + "\n\n"
-        self.rm_patches_msg = "\n\nRemoved the following patch(es):\n"
-
-        super(Recipe, self).__init__()
-
-    def update_env(self, env):
-        self.env = env
-
-    def _rename_files_dir(self, old_ver, new_ver):
-        # The files directory is renamed only if the previous
-        # one has the following format PackageName-PackageVersion.
-        # Otherwise is kept the same way.
-        src_dir = os.path.join(self.recipe_dir, self.env['PN'] + "-" + old_ver)
-        dest_dir = os.path.join(self.recipe_dir, self.env['PN'] + "-" + new_ver)
-
-        if os.path.exists(src_dir) and os.path.isdir(src_dir):
-            self.git.mv(src_dir, dest_dir)
-
-    def rename(self):
-        # change PR before renaming
-        for f in os.listdir(self.recipe_dir):
-            full_path_f = os.path.join(self.recipe_dir, f)
-            if os.path.isfile(full_path_f) and \
-                    ((f.find(self.env['PN']) == 0 and f.find(self.env['PKGV']) != -1 and
-                      f.find(".bb") != -1) or
-                     (f.find(self.env['PN']) == 0 and f.find(".inc") != -1)):
-                with open(full_path_f + ".tmp", "w+") as temp_recipe:
-                    with open(full_path_f) as recipe:
-                        for line in recipe:
-                            if line.startswith("PR=") or line.startswith("PR ="):
-                                continue
-                            else:
-                                temp_recipe.write(line)
-                os.rename(full_path_f + ".tmp", full_path_f)
-
-        # rename recipes (not directories)
-        for path in os.listdir(self.recipe_dir):
-            full_path = os.path.join(self.recipe_dir, path)
-            if os.path.isfile(full_path) \
-              and path.find(self.env['PN']) == 0 \
-              and path.find(self.env['PKGV']) != -1:
-                new_path = re.sub(re.escape(self.env['PKGV']), self.new_ver, path)
-                self.git.mv(os.path.join(self.recipe_dir, path),
-                            os.path.join(self.recipe_dir, new_path))
-
-        # rename files/PN-PV directories to PN
-        self._rename_files_dir(self.env['PKGV'], self.new_ver)
-
-        self.recipes_renamed = True
-
-        # since we did some renaming, backup the current environment
-        self.old_env = self.env
-
-        # start formatting the commit message
-
-    def create_diff_file(self, file, old_md5, new_md5):
-        old_file = os.path.join(self.old_env['S'], file)
-        new_file = os.path.join(self.env['S'], file)
-        cmd = "diff -Nup " + old_file + " " + new_file + " > " + \
-              os.path.join(self.workdir, os.path.basename(file + ".diff"))
-
-        try:
-            stdout, stderr = bb.process.run(cmd)
-        except bb.process.ExecutionError:
-            pass
-
-        with open(os.path.join(self.workdir, "license_checksums.txt"), "w+") as f:
-            f.write("old checksum = %s\n" % old_md5)
-            f.write("new_checksum = %s\n" % new_md5)
-
-        for f in os.listdir(self.recipe_dir):
-            full_path_f = os.path.join(self.recipe_dir, f)
-            if os.path.isfile(full_path_f) and \
-                    ((f.find(self.env['PN']) == 0 and
-                      f.find(self.env['PKGV']) != -1 and
-                      f.find(".bb") != -1) or
-                     (f.find(self.env['PN']) == 0 and
-                      f.find(".inc") != -1)):
-                with open(full_path_f + ".tmp", "w+") as temp_recipe:
-                    with open(full_path_f) as recipe:
-                        for line in recipe:
-                            m = re.match("(.*)" + old_md5 + "(.*)", line)
-                            if m is not None:
-                                temp_recipe.write(m.group(1) + new_md5 + m.group(2) + "\n")
-                            else:
-                                temp_recipe.write(line)
-
-                os.rename(full_path_f + ".tmp", full_path_f)
-
-    def _change_recipe_checksums(self, fetch_log):
-        sums = {}
-
-        with open(os.path.realpath(fetch_log)) as log:
-            for line in log:
-                m = None
-                key = None
-                m1 = re.match("^SRC_URI\[(.*)md5sum\].*", line)
-                m2 = re.match("^SRC_URI\[(.*)sha256sum\].*", line)
-                if m1:
-                    m = m1
-                    key = "md5sum"
-                elif m2:
-                    m = m2
-                    key = "sha256sum"
-
-                if m:
-                    name = m.group(1)
-                    sum_line = m.group(0) + '\n'
-                    if name not in sums:
-                        sums[name] = {}
-                    sums[name][key] = sum_line;
-
-        if len(sums) == 0:
-            raise FetchError()
-
-        I(" %s: Update recipe checksums ..." % self.env['PN'])
-        # checksums are usually in the main recipe but they can also be in inc
-        # files... Go through the recipes/inc files until we find them
-        for f in os.listdir(self.recipe_dir):
-            full_path_f = os.path.join(self.recipe_dir, f)
-            if os.path.isfile(full_path_f) and \
-                    ((f.find(self.env['PN']) == 0 and f.find(self.env['PKGV']) != -1 and
-                      f.find(".bb") != -1) or
-                     (f.find(self.env['PN']) == 0 and f.find(".inc") != -1)):
-                with open(full_path_f + ".tmp", "w+") as temp_recipe:
-                    with open(full_path_f) as recipe:
-                        for line in recipe:
-                            for name in sums:
-                                m1 = re.match("^SRC_URI\["+ name + "md5sum\].*", line)
-                                m2 = re.match("^SRC_URI\["+ name + "sha256sum\].*", line)
-                                if m1:
-                                    temp_recipe.write(sums[name]["md5sum"])
-                                elif m2:
-                                    temp_recipe.write(sums[name]["sha256sum"])
-                                else:
-                                    temp_recipe.write(line)
-
-                os.rename(full_path_f + ".tmp", full_path_f)
-        
-        self.checksums_changed = True
-
-    def _is_uri_failure(self, fetch_log):
-        uri_failure = None
-        checksum_failure = None
-        with open(os.path.realpath(fetch_log)) as log:
-            for line in log:
-                if not uri_failure:
-                    uri_failure = re.match(".*Fetcher failure for URL.*", line)
-                if not checksum_failure:
-                    checksum_failure = re.match(".*Checksum mismatch.*", line)
-        if uri_failure and not checksum_failure:
-            return True
-        else:
-            return False
-
-
-    def _change_source_suffix(self, new_suffix):
-        # Will change the extension of the archive from the SRC_URI
-        for f in os.listdir(self.recipe_dir):
-            full_path_f = os.path.join(self.recipe_dir, f)
-            if os.path.isfile(full_path_f) and \
-                    ((f.find(self.env['PN']) == 0 and f.find(self.env['PKGV']) != -1 and
-                      f.find(".bb") != -1) or
-                     (f.find(self.env['PN']) == 0 and f.find(".inc") != -1)):
-                with open(full_path_f + ".tmp", "w+") as temp_recipe:
-                    with open(full_path_f) as recipe:
-                        source_found = False
-                        for line in recipe:
-                            # source on first line
-                            m1 = re.match("^SRC_URI.*\${PV}\.(.*)[\" \\\\].*", line)
-                            # SRC_URI alone on the first line
-                            m2 = re.match("^SRC_URI.*", line)
-                            # source on second line
-                            m3 = re.match(".*\${PV}\.(.*)[\" \\\\].*", line)
-                            if m1:
-                                old_suffix = m1.group(1)
-                                line = line.replace(old_suffix, new_suffix+" ")
-                            if m2 and not m1:
-                                source_found = True
-                            if m3 and source_found:
-                                old_suffix = m3.group(1)
-                                line = line.replace(old_suffix, new_suffix+" ")
-                                source_found = False
-
-                            temp_recipe.write(line)
-                os.rename(full_path_f + ".tmp", full_path_f)
-
-    def _remove_patch_uri(self, uri):
-        recipe_files = [
-            os.path.join(self.recipe_dir, self.env['PN'] + ".inc"),
-            self.env['FILE']]
-
-        for recipe_filename in recipe_files:
-            if os.path.isfile(recipe_filename):
-                with open(recipe_filename + ".tmp", "w+") as temp_recipe:
-                    with open(recipe_filename) as recipe:
-                        for line in recipe:
-                            if line.find(uri) == -1:
-                                temp_recipe.write(line)
-                                continue
-                            
-                            m1 = re.match("SRC_URI *\+*= *\" *" + uri + " *\"", line)
-                            m2 = re.match("(SRC_URI *\+*= *\" *)" + uri + " *\\\\", line)
-                            m3 = re.match("[\t ]*" + uri + " *\\\\", line)
-                            m4 = re.match("([\t ]*)" + uri + " *\"", line)
-
-                            # patch on a single SRC_URI line:
-                            if m1:
-                                continue
-                            # patch is on the first SRC_URI line
-                            elif m2:
-                                temp_recipe.write(m2.group(1) + "\\\n")
-                            # patch is in the middle
-                            elif m3:
-                                continue
-                            # patch is last in list
-                            elif m4:
-                                temp_recipe.write(m4.group(1) + "\"\n")
-                            # nothing matched in recipe but we deleted the patch
-                            # anyway? Then we must bail out!
-                            else:
-                                return False
-
-                os.rename(recipe_filename + ".tmp", recipe_filename)
-
-        return True
-
-    def _remove_faulty_patch(self, patch_log):
-        patch_file = None
-        is_reverse_applied = False
-
-        with open(patch_log) as log:
-            for line in log:
-                m1 = re.match("^Patch ([^ ]*) does not apply.*", line)
-                m2 = re.match("Patch ([^ ]*) can be reverse-applied", line)
-                if m2:
-                    m1 = m2
-                    is_reverse_applied = True
-                if m1:
-                    patch_file = m1.group(1)
-                    break
-
-        if not patch_file:
-            return False
-
-        I(" %s: Removing patch %s ..." % (self.env['PN'], patch_file))
-        reason = None
-        found = False
-        dirs = [self.env['PN'] + "-" + self.env['PKGV'], self.env['PN'], "files"]
-        for dir in dirs:
-            patch_file_path = os.path.join(self.recipe_dir, dir, patch_file)
-            if not os.path.exists(patch_file_path):
-                continue
-            else:
-                found = True
-                # Find out upstream status of the patch
-                with open(patch_file_path) as patch:
-                    for line in patch:
-                        m = re.match(".*Upstream-Status:(.*)\n", line)
-                        if m:
-                            reason = m.group(1).strip().split()[0].lower()
-                os.remove(patch_file_path)
-                if not self._remove_patch_uri("file://" + patch_file):
-                    return False
-        if not found:
-            return False
-
-        self.rm_patches_msg += " * " + patch_file
-        if reason:
-            self.rm_patches_msg += " (" + reason + ") "
-        if is_reverse_applied:
-            self.rm_patches_msg += "+ reverse-applied"
-        self.rm_patches_msg += "\n"
-        return True
-
-    def _is_license_issue(self, config_log):
-        with open(config_log) as log:
-            for line in log:
-                m = re.match("ERROR: " + self.env['PN'] +
-                             "[^:]*: md5 data is not matching for file", line)
-                if m is not None:
-                    return True
-
-        return False
-
-    def _license_issue_handled(self, config_log):
-        license_file = None
-        with open(config_log) as log:
-            for line in log:
-                if not line.startswith("ERROR:"):
-                    continue
-                m_old = re.match("ERROR: " + self.env['PN'] +
-                        "[^:]*: md5 data is not matching for file://([^;]*);md5=(.*)$", line)
-                if not m_old:
-                    m_old = re.match("ERROR: " + self.env['PN'] +
-                            "[^:]*: md5 data is not matching for file://([^;]*);beginline=[0-9]*;endline=[0-9]*;md5=(.*)$", line)
-                if not m_old:
-                    m_old = re.match("ERROR: " + self.env['PN'] +
-                            "[^:]*: md5 data is not matching for file://([^;]*);endline=[0-9]*;md5=(.*)$", line)
-                if not m_old:
-                    m_old = re.match("ERROR: " + self.env['PN'] +
-                            "[^:]*: md5 data is not matching for file://([^;]*);beginline=[0-9]*;md5=(.*)$", line)
-                m_new = re.match("ERROR: " + self.env['PN'] +
-                        "[^:]*: The new md5 checksum is (.*)", line)
-                if m_old:
-                    license_file = m_old.group(1)
-                    old_md5 = m_old.group(2)
-                elif m_new:
-                    new_md5 = m_new.group(1)
-        
-        if license_file is not None:
-            self.create_diff_file(license_file, old_md5, new_md5)
-            self.license_diff_file = os.path.join(self.workdir, os.path.basename(license_file + ".diff"))
-            if self.interactive:
-                W("  %s: license checksum failed for file %s. The recipe has"
-                  "been updated! View diff? (Y/n)" % (self.env['PN'], license_file))
-                answer = sys.stdin.readline().strip().upper()
-                if answer == '' or answer == 'Y':
-                    I(" ################ Licence file diff #################")
-                    with open(self.license_diff_file) as diff:
-                        I("%s" % diff.read())
-                    I(" ####################################################")
-                I(" Retry compilation? (Y/n)")
-                answer = sys.stdin.readline().strip().upper()
-                if answer == '' or answer == 'Y':
-                    return True
-            else:
-                W(" %s: license checksum failed for file %s."
-                  " The recipe has been updated! Diff file located at %s" %
-                  (self.env['PN'], license_file, self.license_diff_file))
-                I(" Recompiling ...")
-                self.commit_msg += "License checksum changed for file " + license_file
-                return True
-
-        return False
-
-    def get_license_diff_file_name(self):
-        file_name = None
-        if not self.license_diff_file is None:
-            file_name = os.path.basename(self.license_diff_file)
-
-        return file_name
-
-    def _get_failed_recipes(self, output):
-        failed_tasks = dict()
-        machine = None
-
-        for line in output.split("\n"):
-            machine_match = re.match("MACHINE[\t ]+= *\"(.*)\"$", line)
-            task_log_match = re.match("ERROR: Logfile of failure stored in: (.*/([^/]*)/[^/]*/temp/log\.(.*)\.[0-9]*)", line)
-            # For some reason do_package is reported differently
-            qa_issue_match = re.match("ERROR: QA Issue: ([^ :]*): (.*) not shipped", line)
-
-            if task_log_match:
-                failed_tasks[task_log_match.group(2)] = (task_log_match.group(3), task_log_match.group(1))
-            elif qa_issue_match:
-                # Improvise path to log file
-                failed_tasks[qa_issue_match.group(1)] = ("do_package", self.bb.get_stdout_log())
-            elif machine_match:
-                machine = machine_match.group(1)
-
-        # we didn't detect any failed tasks? then something else is wrong
-        if len(failed_tasks) == 0:
-            raise Error("could not detect failed task")
-
-        return (machine, failed_tasks)
-
-    def _is_incompatible_host(self, output):
-        for line in output.split("\n"):
-            incomp_host = re.match("ERROR: " + self.env['PN'] + " was skipped: incompatible with host (.*) \(.*$", line)
-
-            if incomp_host is not None:
-                return True
-
-        return False
-
-    def _add_not_shipped(self, package_log):
-        files_not_shipped = False
-        files = []
-        occurences = []
-        prefixes = {
-          "/usr"            : "prefix",
-          "/bin"            : "base_bindir",
-          "/sbin"           : "base_sbindir",
-          "/lib"            : "base_libdir",
-          "/usr/share"      : "datadir",
-          "/etc"            : "sysconfdir",
-          "/var"            : "localstatedir",
-          "/usr/share/info" : "infodir",
-          "/usr/share/man"  : "mandir",
-          "/usr/share/doc"  : "docdir",
-          "/srv"            : "servicedir",
-          "/usr/bin"        : "bindir",
-          "/usr/sbin"       : "sbindir",
-          "/usr/libexec"    : "libexecdir",
-          "/usr/lib"        : "libdir",
-          "/usr/include"    : "includedir",
-          "/usr/lib/opie"   : "palmtopdir",
-          "/usr/lib/opie"   : "palmqtdir",
-        }
-
-        I(" %s: Add new files in recipe ..." %  self.env['PN'])
-        with open(package_log) as log:
-            for line in log:
-                if re.match(".*Files/directories were installed but not shipped.*", line):
-                    files_not_shipped = True
-                # Extract path
-                line = line.strip()
-                if line:
-                    line = line.split()[0]
-                if files_not_shipped and os.path.isabs(line):
-                    # Count occurences for globbing
-                    path_exists = False
-                    for i in range(0, len(files)):
-                        if line.find(files[i]) == 0:
-                            path_exists = True
-                            occurences[i] += 1
-                            break
-                    if not path_exists:
-                        files.append(line)
-                        occurences.append(1)
-
-        for i in range(0, len(files)):
-            # Change paths to globbing expressions where is the case
-            if occurences[i] > 1:
-                files[i] += "/*"
-            largest_prefix = ""
-            # Substitute prefix
-            for prefix in prefixes:
-                if files[i].find(prefix) == 0 and len(prefix) > len(largest_prefix):
-                    largest_prefix = prefix
-            if largest_prefix:
-                replacement = "${" + prefixes[largest_prefix] + "}"
-                files[i] = files[i].replace(largest_prefix, replacement)
-
-        recipe_files = [
-            os.path.join(self.recipe_dir, self.env['PN'] + ".inc"),
-            self.env['FILE']]
-
-        # Append the new files
-        for recipe_filename in recipe_files:
-            if os.path.isfile(recipe_filename):
-                with open(recipe_filename + ".tmp", "w+") as temp_recipe:
-                    with open(recipe_filename) as recipe:
-                        files_clause = False
-                        for line in recipe:
-                            if re.match("^FILES_\${PN}[ +=].*", line):
-                                files_clause = True
-                                temp_recipe.write(line)
-                                continue
-                            # Get front spacing
-                            if files_clause:
-                                front_spacing = re.sub("[^ \t]", "", line)
-                            # Append once the last line has of FILES has been reached
-                            if re.match(".*\".*", line) and files_clause:
-                                files_clause = False
-                                line = line.replace("\"", "")
-                                line = line.rstrip()
-                                front_spacing = re.sub("[^ \t]", "", line)
-                                # Do not write an empty line
-                                if line.strip():
-                                    temp_recipe.write(line + " \\\n")
-                                # Add spacing in case there was none
-                                if len(front_spacing) == 0:
-                                    front_spacing = " " * 8
-                                # Write to file
-                                for i in range(len(files)-1):
-                                    line = front_spacing + files[i] + " \\\n"
-                                    temp_recipe.write(line)
-
-                                line = front_spacing + files[len(files) - 1] + "\"\n"
-                                temp_recipe.write(line)
-                                continue
-
-                            temp_recipe.write(line)
-
-                os.rename(recipe_filename + ".tmp", recipe_filename)
-
-    def unpack(self):
-        self.bb.unpack(self.env['PN'])
-
-    def fetch(self):
-        from gitrecipe import GitRecipe
-
-        def _try_fetch():
-            try:
-                self.bb.fetch(self.env['PN'])
-                return
-            except Error as e:
-                machine, failed_recipes = self._get_failed_recipes(e.stdout)
-                if not self.env['PN'] in failed_recipes:
-                    raise Error("Unknown error occured during fetch",
-                            stdout = e.stdout, stderr = e.stderr)
-
-                fetch_log = failed_recipes[self.env['PN']][1]
-
-                if not self._is_uri_failure(fetch_log) and not \
-                        self.checksums_changed:
-                    self._change_recipe_checksums(fetch_log)
-                    self.checksums_changed = True
-                    return True
-
-                return False
-
-        succeed = _try_fetch()
-        if not succeed and not isinstance(self, GitRecipe):
-            for sfx in self.suffixes:
-                I(" Trying new SRC_URI suffix: %s ..." % sfx)
-                self._change_source_suffix(sfx)
-
-                succeed = _try_fetch()
-                if succeed:
-                    break
-
-        if not succeed:
-            raise Error("Can't built a valid SRC_URI")
-        elif self.recipes_renamed and not self.checksums_changed:
-            raise Error("Fetch succeeded without changing checksums")
-
-    def cleanall(self):
-        self.bb.cleanall(self.env['PN'])
-
-    def _clean_failed_recipes(self, failed_recipes):
-        already_retried = False
-        for recipe in failed_recipes:
-            if recipe in self.retried_recipes:
-                # we already retried, we'd best leave it to a human to handle
-                # it :)
-                already_retried = True
-            # put the recipe in the retried list
-            self.retried_recipes.add(recipe)
-
-        if already_retried:
-            return False
-        else:
-            I(" %s: The following recipe(s): %s, failed.  "
-              "Doing a 'cleansstate' and then retry ..." %
-              (self.env['PN'], ' '.join(failed_recipes.keys())))
-
-            self.bb.cleansstate(' '.join(failed_recipes.keys()))
-            return True
-
-    def _undo_temporary(self):
-        # Undo removed patches
-        if self.removed_patches:
-            self.git.checkout_branch("upgrades")
-            self.git.delete_branch("remove_patches")
-            self.git.reset_hard()
-            self.git.reset_soft(1)
-            self.removed_patches = False
-
-    def compile(self, machine):
-        try:
-            self.bb.complete(self.env['PN'], machine)
-            if self.removed_patches:
-                # move temporary changes into upgrades branch
-                self.git.checkout_branch("upgrades")
-                self.git.delete_branch("remove_patches")
-                self.git.reset_soft(1)
-                self.commit_msg += self.rm_patches_msg + "\n"
-                self.removed_patches = False
-        except Error as e:
-            if self._is_incompatible_host(e.stdout):
-                W(" %s: compilation failed: incompatible host" % self.env['PN'])
-                return
-            machine, failed_recipes = self._get_failed_recipes(e.stdout)
-            if not self.env['PN'] in failed_recipes:
-                if not self._clean_failed_recipes(failed_recipes):
-                    self._undo_temporary()
-                    raise CompilationError()
-
-                # retry
-                self.compile(machine)
-            else:
-                failed_task = failed_recipes[self.env['PN']][0]
-                log_file = failed_recipes[self.env['PN']][1]
-                if failed_task == "do_patch":
-                    # Remove one patch after the other until
-                    # compilation works.
-                    if not self.removed_patches:
-                        self.git.commit("temporary")
-                        self.git.create_branch("remove_patches")
-                        self.git.checkout_branch("remove_patches")
-                        self.removed_patches = True
-                    if not self._remove_faulty_patch(log_file):
-                        self._undo_temporary()
-                        raise PatchError()
-
-                    # retry
-                    I(" %s: Recompiling for %s ..." % (self.env['PN'], machine))
-                    self.compile(machine)
-                elif failed_task == "do_configure":
-                    self._undo_temporary()
-                    if not self._is_license_issue(log_file):
-                        raise ConfigureError()
-
-                    if not self._license_issue_handled(log_file):
-                        raise LicenseError()
-                    #retry
-                    self.compile(machine)
-                elif failed_task == "do_fetch":
-                    raise FetchError()
-                elif failed_task == "do_package":
-                    self._add_not_shipped(log_file)
-                    self.compile(machine)
-                else:
-                    self._undo_temporary()
-                    # throw a compilation exception for everything else. It
-                    # doesn't really matter
-                    raise CompilationError()
diff --git a/statistics.py b/statistics.py
deleted file mode 100644
index 32a6748..0000000
--- a/statistics.py
+++ /dev/null
@@ -1,102 +0,0 @@
-#!/usr/bin/env python
-# vim: set ts=4 sw=4 et:
-#
-# Copyright (c) 2013 - 2014 Intel Corporation
-#
-# This program is free software; you can redistribute it and/or
-# modify it under the terms of the GNU General Public License
-# as published by the Free Software Foundation; either version 2
-# of the License, or (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
-#
-# AUTHORS
-# Laurentiu Palcu   <laurentiu.palcu at intel.com>
-# Marius Avram      <marius.avram at intel.com>
-#
-
-class Statistics(object):
-    def __init__(self):
-        self.succeeded = dict()
-        self.failed = dict()
-        self.succeeded["total"] = 0
-        self.failed["total"] = 0
-        self.upgrade_stats = dict()
-        self.maintainers = set()
-        self.total_attempted = 0
-
-    def update(self, pn, new_ver, maintainer, error):
-        if type(error).__name__ == "UpgradeNotNeededError":
-            return
-        elif error is None:
-            status = "Succeeded"
-        else:
-            status = str(error)
-
-        if not status in self.upgrade_stats:
-            self.upgrade_stats[status] = []
-
-        self.upgrade_stats[status].append((pn, new_ver, maintainer))
-
-        # add maintainer to the set of unique maintainers
-        self.maintainers.add(maintainer)
-
-        if not maintainer in self.succeeded:
-            self.succeeded[maintainer] = 0
-        if not maintainer in self.failed:
-            self.failed[maintainer] = 0
-
-        if status == "Succeeded":
-            self.succeeded["total"] += 1
-            self.succeeded[maintainer] += 1
-        else:
-            self.failed["total"] += 1
-            self.failed[maintainer] += 1
-
-        self.total_attempted += 1
-
-    def pkg_stats(self):
-        stat_msg = "\nUpgrade statistics:\n"
-        stat_msg += "====================================================\n"
-        for status in self.upgrade_stats:
-            list_len = len(self.upgrade_stats[status])
-            if list_len > 0:
-                stat_msg += "* " + status + ": " + str(list_len) + "\n"
-
-                for pkg, new_ver, maintainer in self.upgrade_stats[status]:
-                    stat_msg += "    " + pkg + ", " + new_ver + ", " + \
-                                maintainer + "\n"
-
-        if self.total_attempted == 0:
-            percent_succeded = 0
-            percent_failed = 0
-        else:
-            percent_succeded = self.succeeded["total"] * 100.0 / self.total_attempted
-            percent_failed = self.failed["total"] * 100.0 / self.total_attempted
-        stat_msg += "++++++++++++++++++++++++++++++++++++++++++++++++++++\n"
-        stat_msg += "TOTAL: attempted=%d succeeded=%d(%.2f%%) failed=%d(%.2f%%)\n\n" % \
-                    (self.total_attempted, self.succeeded["total"],
-                    percent_succeded,
-                    self.failed["total"],
-                    percent_failed)
-
-        return stat_msg
-
-    def maintainer_stats(self):
-        stat_msg = "* Statistics per maintainer:\n"
-        for m in self.maintainers:
-            attempted = self.succeeded[m] + self.failed[m]
-            stat_msg += "    %s: attempted=%d succeeded=%d(%.2f%%) failed=%d(%.2f%%)\n\n" % \
-                        (m.split("@")[0], attempted, self.succeeded[m],
-                        self.succeeded[m] * 100.0 / attempted,
-                        self.failed[m],
-                        self.failed[m] * 100.0 / attempted)
-
-        return stat_msg
diff --git a/steps.py b/steps.py
deleted file mode 100644
index ea314da..0000000
--- a/steps.py
+++ /dev/null
@@ -1,142 +0,0 @@
-#!/usr/bin/env python
-# vim: set ts=4 sw=4 et:
-#
-# Copyright (c) 2013 - 2015 Intel Corporation
-#
-# This program is free software; you can redistribute it and/or
-# modify it under the terms of the GNU General Public License
-# as published by the Free Software Foundation; either version 2
-# of the License, or (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
-#
-
-import os
-import sys
-
-from logging import debug as D
-from logging import info as I
-from logging import warning as W
-from logging import error as E
-from logging import critical as C
-
-from errors import *
-from buildhistory import BuildHistory
-from recipe import Recipe
-from gitrecipe import GitRecipe
-from svnrecipe import SvnRecipe
-
-def load_env(bb, git, opts, pkg_ctx):
-    stdout = git.status()
-    if stdout != "":
-        if opts['interactive']:
-            W(" %s: git repository has uncommited work which will be dropped!" \
-                    " Proceed? (y/N)" % pkg_ctx['PN'])
-            answer = sys.stdin.readline().strip().upper()
-            if answer == '' or answer != 'Y':
-                I(" %s: User abort!" % pkg_ctx['PN'])
-                exit(1)
-
-        I(" %s: Dropping uncommited work!" % pkg_ctx['PN'])
-        git.reset_hard()
-        git.clean_untracked()
-
-    pkg_ctx['env'] = bb.env(pkg_ctx['PN'])
-    if pkg_ctx['env']['PV'] == pkg_ctx['NPV']:
-        raise UpgradeNotNeededError
-
-def load_dirs(bb, git, opts, pkg_ctx):
-    pkg_ctx['workdir'] = os.path.join(pkg_ctx['base_dir'], pkg_ctx['PN'])
-    os.mkdir(pkg_ctx['workdir'])
-
-    pkg_ctx['recipe_dir'] = os.path.dirname(pkg_ctx['env']['FILE'])
-
-def clean_repo(bb, git, opts, pkg_ctx):
-    try:
-        git.checkout_branch("upgrades")
-    except Error:
-        git.create_branch("upgrades")
-
-    try:
-        git.delete_branch("remove_patches")
-    except:
-        pass
-
-def detect_recipe_type(bb, git, opts, pkg_ctx):
-    if pkg_ctx['env']['SRC_URI'].find("ftp://") != -1 or  \
-            pkg_ctx['env']['SRC_URI'].find("http://") != -1 or \
-            pkg_ctx['env']['SRC_URI'].find("https://") != -1:
-        recipe = Recipe
-    elif pkg_ctx['env']['SRC_URI'].find("git://") != -1:
-        recipe = GitRecipe
-    else:
-        raise UnsupportedProtocolError
-
-    pkg_ctx['recipe'] = recipe(pkg_ctx['env'], pkg_ctx['NPV'],
-            opts['interactive'], pkg_ctx['workdir'],
-            pkg_ctx['recipe_dir'], bb, git)
-
-def buildhistory_init(bb, git, opts, pkg_ctx):
-    if not opts['buildhistory_enabled']:
-        return
-
-    pkg_ctx['buildhistory'] = BuildHistory(bb, pkg_ctx['PN'],
-            pkg_ctx['workdir'])
-    I(" %s: Initial buildhistory for %s ..." % (pkg_ctx['PN'],
-            opts['machines']))
-    pkg_ctx['buildhistory'].init(opts['machines'])
-
-def unpack_original(bb, git, opts, pkg_ctx):
-    pkg_ctx['recipe'].unpack()
-
-def rename(bb, git, opts, pkg_ctx):
-    pkg_ctx['recipe'].rename()
-
-    pkg_ctx['env'] = bb.env(pkg_ctx['PN'])
-
-    pkg_ctx['recipe'].update_env(pkg_ctx['env'])
-
-def cleanall(bb, git, opts, pkg_ctx):
-    pkg_ctx['recipe'].cleanall()
-
-def fetch(bb, git, opts, pkg_ctx):
-    pkg_ctx['recipe'].fetch()
-
-def compile(bb, git, opts, pkg_ctx):
-    if opts['skip_compilation']:
-        W(" %s: Compilation was skipped by user choice!")
-        return
-
-    for machine in opts['machines']:
-        I(" %s: compiling for %s ..." % (pkg_ctx['PN'], machine))
-        pkg_ctx['recipe'].compile(machine)
-        if opts['buildhistory_enabled']:
-            pkg_ctx['buildhistory'].add()
-
-def buildhistory_diff(bb, git, opts, pkg_ctx):
-    if not opts['buildhistory_enabled']:
-        return
-
-    I(" %s: Checking buildhistory ..." % pkg_ctx['PN'])
-    pkg_ctx['buildhistory'].diff()
-
-upgrade_steps = [
-    (load_env, "Loading environment ..."),
-    (load_dirs, None),
-    (clean_repo, "Cleaning git repository of temporary branch ..."),
-    (detect_recipe_type, None),
-    (buildhistory_init, None),
-    (unpack_original, "Fetch & unpack original version ..."),
-    (rename, "Renaming recipes, reset PR (if exists) ..."),
-    (cleanall, "Clean all ..."),
-    (fetch, "Fetch new version (old checksums) ..."),
-    (compile, None),
-    (buildhistory_diff, None)
-]
diff --git a/svnrecipe.py b/svnrecipe.py
deleted file mode 100644
index 7d8ad16..0000000
--- a/svnrecipe.py
+++ /dev/null
@@ -1,28 +0,0 @@
-#!/usr/bin/env python
-# vim: set ts=4 sw=4 et:
-#
-# Copyright (c) 2013 - 2014 Intel Corporation
-#
-# This program is free software; you can redistribute it and/or
-# modify it under the terms of the GNU General Public License
-# as published by the Free Software Foundation; either version 2
-# of the License, or (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program; if not, write to the Free Software
-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
-#
-# AUTHORS
-# Laurentiu Palcu   <laurentiu.palcu at intel.com>
-# Marius Avram      <marius.avram at intel.com>
-#
-
-from recipe import Recipe
-
-class SvnRecipe(Recipe):
-    pass
diff --git a/upgradehelper.py b/upgradehelper.py
index 22365b7..e526597 100755
--- a/upgradehelper.py
+++ b/upgradehelper.py
@@ -44,14 +44,17 @@ import ConfigParser as cp
 from datetime import datetime
 from datetime import date
 import shutil
+
+sys.path.insert(1, os.path.join(os.path.abspath(
+    os.path.dirname(__file__)), 'modules'))
+
 from errors import *
 
-from git import Git
-from bitbake import Bitbake
+from utils.git import Git
+from utils.bitbake import Bitbake
+from utils.emailhandler import Email
 
-from emailhandler import Email
 from statistics import Statistics
-
 from steps import upgrade_steps
 
 help_text = """Usage examples:
-- 
2.1.4




More information about the yocto mailing list