From 8c4c4394730bf77d44f831ca97d565bc2fb14e47 Mon Sep 17 00:00:00 2001
From: Christophe Monniez <moc@odoo.com>
Date: Fri, 27 Sep 2019 10:01:58 +0000
Subject: [PATCH] [REF] packaging: use docker to build packages

Actually, docker is used to test Linux packages once they are built.
The packaging process is done on the host system where package.py is
run.

Consequences:
    * difficult to reproduce an exact same build on another host
    * changing a Debian/Ubuntu target version means some upgrade steps
      on the build host (mix that with the fact that the host also
      could build old Odoo versions)
    * Fedora rpm package is built on an Ubuntu system (which is not a
      real problem but it could be)

With this commit, the package.py utility is refactored to use Docker
containers to build the Gnu/Linux packages.

Also, the Debian package is adapted for Debian Buster, the RPM package
is built based on Fedora 30.

Some minor changes are also made in the windows packaging system.

closes odoo/odoo#37766

Signed-off-by: Christophe Monniez (moc) <moc@odoo.com>
---
 setup.cfg                   |   4 +
 setup/package.dfdebian      |   8 +-
 setup/package.dffedora      |   8 +-
 setup/package.dfsrc         |   4 +-
 setup/package.py            | 859 +++++++++++++++++++-----------------
 setup/redhat/install.sh     |   2 +-
 setup/redhat/postinstall.sh |   8 +-
 7 files changed, 463 insertions(+), 430 deletions(-)

diff --git a/setup.cfg b/setup.cfg
index 5b0fa9220222..ffccdc094ac5 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,3 +1,6 @@
+[install]
+optimize=1
+
 [bdist_rpm]
 no-autoreq = yes
 install-script = setup/redhat/install.sh
@@ -48,3 +51,4 @@ requires =
   python3-werkzeug
   python3-xlwt
   python3-xlrd
+
diff --git a/setup/package.dfdebian b/setup/package.dfdebian
index a57981c11ab6..433ea576714a 100644
--- a/setup/package.dfdebian
+++ b/setup/package.dfdebian
@@ -1,6 +1,6 @@
 # Please note that this Dockerfile is used for testing nightly builds and should
 # not be used to deploy Odoo
-FROM debian:stretch
+FROM debian:buster
 MAINTAINER Odoo S.A. <info@odoo.com>
 
 RUN apt-get update && \
@@ -12,11 +12,14 @@ RUN dpkg-reconfigure locales && \
     locale-gen C.UTF-8 && \
     /usr/sbin/update-locale LANG=C.UTF-8
 ENV LC_ALL C.UTF-8
+ENV DEBIAN_FRONTEND noninteractive
 
 RUN apt-get update -qq &&  \
     apt-get upgrade -qq -y && \
     apt-get install -qq -y\
         adduser \
+        dh-python \
+        packaging-dev \
         postgresql \
         postgresql-client \
         python3 \
@@ -51,7 +54,8 @@ RUN apt-get update -qq &&  \
         python3-vatnumber \
         python3-vobject \
         python3-werkzeug \
-        python3-xlsxwriter && \
+        python3-xlsxwriter \
+        rsync && \
     rm -rf /var/lib/apt/lists/*
 
 RUN echo "PS1=\"[\u@nightly-tests] # \"" > ~/.bashrc
diff --git a/setup/package.dffedora b/setup/package.dffedora
index b455b49db3c1..6f745e1b5335 100644
--- a/setup/package.dffedora
+++ b/setup/package.dffedora
@@ -1,6 +1,6 @@
 # Please note that this Dockerfile is used for testing nightly builds and should
 # not be used to deploy Odoo
-FROM fedora:26
+FROM fedora:30
 MAINTAINER Odoo S.A. <info@odoo.com>
 
 # Dependencies and postgres
@@ -45,17 +45,17 @@ RUN dnf update -d 0 -e 0 -y && \
 	  python3-werkzeug \
 	  python3-xlwt \
 	  python3-xlrd \
+		python3-xlsxwriter \
 		libsass \
 		pytz \
 		postgresql \
 		postgresql-server \
 		postgresql-libs \
 		postgresql-contrib \
-		postgresql-devel -y && \
+		postgresql-devel \
+		rpmdevtools -y && \
 	dnf clean all
 
-RUN pip3 install XlsxWriter
-
 # Postgres configuration
 RUN mkdir -p /var/lib/postgres/data
 RUN chown -R postgres:postgres /var/lib/postgres/data
diff --git a/setup/package.dfsrc b/setup/package.dfsrc
index 5b106923047c..e37ffcc9e29c 100644
--- a/setup/package.dfsrc
+++ b/setup/package.dfsrc
@@ -1,6 +1,6 @@
 # Please note that this Dockerfile is used for testing nightly builds and should
 # not be used to deploy Odoo
-FROM debian:stretch
+FROM debian:buster
 MAINTAINER Odoo S.A. <info@odoo.com>
 
 RUN apt-get update && \
@@ -20,7 +20,7 @@ RUN apt-get update -qq &&  \
 		postgresql-server-dev-all \
 		postgresql-client \
 		adduser \
-		libsass0 \
+		libsass1 \
 		libxml2-dev \
 		libxslt1-dev \
 		libldap2-dev \
diff --git a/setup/package.py b/setup/package.py
index 36029881bdd6..9bdaf2991267 100755
--- a/setup/package.py
+++ b/setup/package.py
@@ -1,89 +1,61 @@
 #!/usr/bin/env python3
 # -*- coding: utf-8 -*-
 # Part of Odoo. See LICENSE file for full copyright and licensing details.
-from __future__ import print_function
+
+import argparse
 import logging
-import optparse
 import os
 import pexpect
 import shutil
 import signal
 import subprocess
+import sys
 import tempfile
 import time
 import traceback
-try:
-    from xmlrpc import client as xmlrpclib
-except ImportError:
-    import xmlrpclib
-from contextlib import contextmanager
-from glob import glob
-from os.path import abspath, dirname, join
-from sys import stdout, stderr
-from tempfile import NamedTemporaryFile
+from xmlrpc import client as xmlrpclib
 
-# apt-get install rsync python-pexpect debhelper python-setuptools
+from glob import glob
 
 #----------------------------------------------------------
 # Utils
 #----------------------------------------------------------
-exec(open(join(dirname(__file__), '..', 'odoo', 'release.py'), 'rb').read())
-version = version.split('-')[0].replace('saas~','')
-docker_version = version.replace('+', '')
-timestamp = time.strftime("%Y%m%d", time.gmtime())
+
+ROOTDIR = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
+TSTAMP = time.strftime("%Y%m%d", time.gmtime())
+exec(open(os.path.join(ROOTDIR, 'odoo', 'release.py'), 'rb').read())
+VERSION = version.split('-')[0].replace('saas~', '')
 GPGPASSPHRASE = os.getenv('GPGPASSPHRASE')
 GPGID = os.getenv('GPGID')
-PUBLISH_DIRS = {
-    'debian': 'deb',
-    'redhat': 'rpm',
-    'tarball': 'src',
-    'windows': 'exe',
-}
-ADDONS_NOT_TO_PUBLISH = [
-]
-
-
-def move_glob(source, wildcards, destination):
-    """Move files matched by wildcards from source to destination
-    wildcards can be a single string wildcard like '*.deb' or a list of wildcards
-    """
-    if not os.path.isdir(destination):
-        raise BaseException('Destination "{}" is not a directory'.format(destination))
-    if isinstance(wildcards, str):
-        wildcards = [wildcards]
-    for wc in wildcards:
-        for file_path in glob(os.path.join(source, wc)):
-            shutil.move(file_path, destination)
-
-def mkdir(d):
-    if not os.path.isdir(d):
-        os.makedirs(d)
-
-def system(l, chdir=None):
-    logging.info("System call: {}".format(l))
-    if chdir:
-        cwd = os.getcwd()
-        os.chdir(chdir)
-    if isinstance(l, list):
-        rc = os.spawnvp(os.P_WAIT, l[0], l)
-    elif isinstance(l, str):
-        tmp = ['sh', '-c', l]
-        rc = os.spawnvp(os.P_WAIT, tmp[0], tmp)
-    if chdir:
-        os.chdir(cwd)
-    return rc
+DOCKERVERSION = VERSION.replace('+', '')
+INSTALL_TIMEOUT = 600
+
+DOCKERUSER = """
+RUN mkdir /var/lib/odoo && \
+    groupadd -g %(group_id)s odoo && \
+    useradd -u %(user_id)s -g odoo odoo -d /var/lib/odoo && \
+    mkdir /data && \
+    chown odoo:odoo /var/lib/odoo /data
+USER odoo
+""" % {'group_id': os.getgid(), 'user_id': os.getuid()}
+
+
+def run_cmd(cmd, chdir=None, timeout=None):
+    logging.info("Running command %s", cmd)
+    return subprocess.run(cmd, cwd=chdir, timeout=timeout)
+
 
 def _rpc_count_modules(addr='http://127.0.0.1', port=8069, dbname='mycompany'):
     time.sleep(5)
-    uid = xmlrpclib.ServerProxy('%s:%s/xmlrpc/common' % (addr, port)).authenticate(
+    uid = xmlrpclib.ServerProxy('%s:%s/xmlrpc/2/common' % (addr, port)).authenticate(
         dbname, 'admin', 'admin', {}
     )
-    modules = xmlrpclib.ServerProxy('%s:%s/xmlrpc/object' % (addr, port)).execute(
+    modules = xmlrpclib.ServerProxy('%s:%s/xmlrpc/2/object' % (addr, port)).execute(
         dbname, uid, 'admin', 'ir.module.module', 'search', [('state', '=', 'installed')]
     )
-    if modules and len(modules) > 1:
+    if len(modules) > 1:
         time.sleep(1)
-        toinstallmodules = xmlrpclib.ServerProxy('%s:%s/xmlrpc/object' % (addr, port)).execute(
+        toinstallmodules = xmlrpclib.ServerProxy('%s:%s/xmlrpc/2/object' % (addr, port)).execute(
             dbname, uid, 'admin', 'ir.module.module', 'search', [('state', '=', 'to install')]
         )
         if toinstallmodules:
@@ -93,22 +65,26 @@ def _rpc_count_modules(addr='http://127.0.0.1', port=8069, dbname='mycompany'):
             logging.info("Package test: successfuly installed %s modules" % len(modules))
     else:
         logging.error("Package test: FAILED. Not able to install base.")
-        raise Exception("Installation of package failed")
+        raise Exception("Package test: FAILED. Not able to install base.")
 
-def publish(o, type, extensions):
-    def _publish(o, release):
-        arch = ''
-        filename = release.split(os.path.sep)[-1]
-
-        release_dir = PUBLISH_DIRS[type]
-        release_path = join(o.pub, release_dir, filename)
 
-        system('mkdir -p %s' % join(o.pub, release_dir))
-        shutil.move(join(o.build_dir, release), release_path)
+def publish(args, pub_type, extensions):
+    """Publish builded package (move builded files and generate a symlink to the latests)
+    :args: parsed program args
+    :pub_type: one of [deb, rpm, src, exe]
+    :extensions: list of extensions to publish
+    :returns: published files
+    """
+    def _publish(release):
+        build_path = os.path.join(args.build_dir, release)
+        filename = release.split(os.path.sep)[-1]
+        release_dir = os.path.join(args.pub, pub_type)
+        release_path = os.path.join(release_dir, filename)
+        os.renames(build_path, release_path)
 
         # Latest/symlink handler
-        release_abspath = abspath(release_path)
-        latest_abspath = release_abspath.replace(timestamp, 'latest')
+        release_abspath = os.path.abspath(release_path)
+        latest_abspath = release_abspath.replace(TSTAMP, 'latest')
 
         if os.path.islink(latest_abspath):
             os.unlink(latest_abspath)
@@ -119,82 +95,307 @@ def publish(o, type, extensions):
 
     published = []
     for extension in extensions:
-        release = glob("%s/odoo_*.%s" % (o.build_dir, extension))
+        release = glob("%s/odoo_*.%s" % (args.build_dir, extension))
         if release:
-            published.append(_publish(o, release[0]))
+            published.append(_publish(release[0]))
     return published
 
-class OdooDocker(object):
-    def __init__(self):
-        self.log_file = NamedTemporaryFile(mode='w+b', prefix="bash", suffix=".txt", delete=False)
-        self.port = 8069  # TODO sle: reliable way to get a free port?
-        self.prompt_re = '[root@nightly-tests] # '
-        self.timeout = 600
-
-    def system(self, command):
-        self.docker.sendline(command)
-        self.docker.expect_exact(self.prompt_re)
-
-    def start(self, docker_image, build_dir, pub_dir):
-        self.build_dir = build_dir
-        self.pub_dir = pub_dir
-
-        self.docker = pexpect.spawn(
-            'docker run -v %s:/opt/release -p 127.0.0.1:%s:8069'
-            ' -t -i %s /bin/bash --noediting' % (self.build_dir, self.port, docker_image),
-            timeout=self.timeout,
-            searchwindowsize=len(self.prompt_re) + 1,
-        )
-        time.sleep(2)  # let the bash start
-        self.docker.logfile_read = self.log_file
-        self.id = subprocess.check_output('docker ps -l -q', shell=True).strip().decode('ascii')
 
-    def end(self):
-        try:
-            _rpc_count_modules(port=str(self.port))
-        except Exception as e:
-            logging.error('Exception during docker execution: %s:' % str(e))
-            logging.error('Error during docker execution: printing the bash output:')
-            with open(self.log_file.name) as f:
-                print('\n'.join(f.readlines()), file=stderr)
-            raise
-        finally:
-            self.docker.close()
-            system('docker rm -f %s' % self.id)
-            self.log_file.close()
-            os.remove(self.log_file.name)
-
-@contextmanager
-def docker(docker_image, build_dir, pub_dir):
-    _docker = OdooDocker()
-    try:
-        _docker.start(docker_image, build_dir, pub_dir)
-        try:
-            yield _docker
-        except Exception:
-            raise
-    finally:
-        _docker.end()
+# ---------------------------------------------------------
+# Generates Packages, Sources and Release files of debian package
+# ---------------------------------------------------------
+def gen_deb_package(args, published_files):
+    # Executes command to produce file_name in path, and moves it to args.pub/deb
+    def _gen_file(args, command, file_name, path):
+        cur_tmp_file_path = os.path.join(path, file_name)
+        with open(cur_tmp_file_path, 'w') as out:
+            subprocess.call(command, stdout=out, cwd=path)
+        shutil.copy(cur_tmp_file_path, os.path.join(args.pub, 'deb', file_name))
+
+    # Copy files to a temp directory (required because the working directory must contain only the
+    # files of the last release)
+    temp_path = tempfile.mkdtemp(suffix='debPackages')
+    for pub_file_path in published_files:
+        shutil.copy(pub_file_path, temp_path)
+
+    commands = [
+        (['dpkg-scanpackages', '.'], "Packages"),  # Generate Packages file
+        (['dpkg-scansources', '.'], "Sources"),  # Generate Sources file
+        (['apt-ftparchive', 'release', '.'], "Release")  # Generate Release file
+    ]
+    # Generate files
+    for command in commands:
+        _gen_file(args, command[0], command[-1], temp_path)
+    # Remove temp directory
+    shutil.rmtree(temp_path)
+
+    if args.sign:
+        # Generate Release.gpg (= signed Release)
+        # Options -abs: -a (Create ASCII armored output), -b (Make a detach signature), -s (Make a signature)
+        subprocess.call(['gpg', '--default-key', GPGID, '--passphrase', GPGPASSPHRASE, '--yes', '-abs', '--no-tty', '-o', 'Release.gpg', 'Release'], cwd=os.path.join(args.pub, 'deb'))
+
+
+# ---------------------------------------------------------
+# Generates an RPM repo
+# ---------------------------------------------------------
+def gen_rpm_repo(args, file_name):
+    """Genereate a rpm repo in publish directory"""
+    # Sign the RPM
+    rpmsign = pexpect.spawn('/bin/bash', ['-c', 'rpm --resign %s' % file_name], cwd=os.path.join(args.pub, 'rpm'))
+    rpmsign.expect_exact('Enter pass phrase: ')
+    rpmsign.send(GPGPASSPHRASE + '\r\n')
+    rpmsign.expect(pexpect.EOF)
+
+    # Removes the old repodata
+    shutil.rmtree(os.path.join(args.pub, 'rpm', 'repodata'))
+
+    # Copy files to a temp directory (required because the working directory must contain only the
+    # files of the last release)
+    temp_path = tempfile.mkdtemp(suffix='rpmPackages')
+    shutil.copy(file_name, temp_path)
+
+    run_cmd(['createrepo', temp_path]).check_returncode()  # creates a repodata folder in temp_path
+    shutil.copytree(os.path.join(temp_path, "repodata"), os.path.join(args.pub, 'rpm'))
+
+    # Remove temp directory
+    shutil.rmtree(temp_path)
+
+
+def _prepare_build_dir(args, win32=False):
+    """Copy files to the build directory"""
+    logging.info('Preparing build dir "%s"', args.build_dir)
+    cmd = ['rsync', '-a', '--delete', '--exclude', '.git', '--exclude', '*.pyc', '--exclude', '*.pyo']
+    if win32 is False:
+        cmd += ['--exclude', 'setup/win32']
+
+    run_cmd(cmd + ['%s/' % args.odoo_dir, args.build_dir])
+    for addon_path in glob(os.path.join(args.build_dir, 'addons/*')):
+        if args.blacklist is None or os.path.basename(addon_path) not in args.blacklist:
+            try:
+                shutil.move(addon_path, os.path.join(args.build_dir, 'odoo/addons'))
+            except shutil.Error as e:
+                logging.warning("Warning '%s' while moving addon '%s", e, addon_path)
+                if addon_path.startswith(args.build_dir) and os.path.isdir(addon_path):
+                    logging.info("Removing ''".format(addon_path))
+                    try:
+                        shutil.rmtree(addon_path)
+                    except shutil.Error as rm_error:
+                        logging.warning("Cannot remove '{}': {}".format(addon_path, rm_error))
+
 
+#  Docker stuffs
+class OdooTestTimeoutError(Exception):
+    pass
+
+
+class OdooTestError(Exception):
+    pass
+
+
+class Docker():
+    """Base Docker class. Must be inherited by specific Docker builder class"""
+    arch = None
+
+    def __init__(self, args):
+        """
+        :param args: argparse parsed arguments
+        """
+        self.args = args
+        self.tag = 'odoo-%s-%s-nightly-tests' % (DOCKERVERSION, self.arch)
+        self.container_name = None
+        self.exposed_port = None
+        dockerfiles = {
+            'tgz': os.path.join(args.build_dir, 'setup/package.dfsrc'),
+            'deb': os.path.join(args.build_dir, 'setup/package.dfdebian'),
+            'rpm': os.path.join(args.build_dir, 'setup/package.dffedora'),
+        }
+        self.dockerfile = dockerfiles[self.arch]
+        self.test_log_file = '/data/src/test-%s.log' % self.arch
+        self.build_image()
+
+    def build_image(self):
+        """Build the dockerimage by copying Dockerfile into build_dir/docker"""
+        docker_dir = os.path.join(self.args.build_dir, 'docker')
+        docker_file_path = os.path.join(docker_dir, 'Dockerfile')
+        os.mkdir(docker_dir)
+        shutil.copy(self.dockerfile, docker_file_path)
+        with open(docker_file_path, 'a') as dockerfile:
+            dockerfile.write(DOCKERUSER)
+        shutil.copy(os.path.join(self.args.build_dir, 'requirements.txt'), docker_dir)
+        run_cmd(["docker", "build", "--rm=True", "-t", self.tag, "."], chdir=docker_dir, timeout=1200).check_returncode()
+        shutil.rmtree(docker_dir)
+
+    def run(self, cmd, build_dir, container_name, user='odoo', exposed_port=None, detach=False, timeout=None):
+        self.container_name = container_name
+        docker_cmd = [
+            "docker",
+            "run",
+            "--user=%s" % user,
+            "--name=%s" % container_name,
+            "--rm",
+            "--volume=%s:/data/src" % build_dir
+        ]
+
+        if exposed_port:
+            docker_cmd.extend(['-p', '127.0.0.1:%s:%s' % (exposed_port, exposed_port)])
+            self.exposed_port = exposed_port
+        if detach:
+            docker_cmd.append('-d')
+            # preserve logs in case of detached docker container
+            cmd = '(%s) > %s 2>&1' % (cmd, self.test_log_file)
+
+        docker_cmd.extend([
+            self.tag,
+            "/bin/bash",
+            "-c",
+            "cd /data/src && %s" % cmd
+        ])
+        run_cmd(docker_cmd, timeout=timeout).check_returncode()
+
+    def is_running(self):
+        dinspect = subprocess.run(['docker', 'container', 'inspect', self.container_name], stderr=subprocess.DEVNULL, stdout=subprocess.DEVNULL)
+        return True if dinspect.returncode == 0 else False
+
+    def stop(self):
+        run_cmd(["docker", "stop", self.container_name]).check_returncode()
+
+    def test_odoo(self):
+        logging.info('Starting to test Odoo install test')
+        start_time = time.time()
+        while self.is_running() and (time.time() - start_time) < INSTALL_TIMEOUT:
+            time.sleep(5)
+            if os.path.exists(os.path.join(args.build_dir, 'odoo.pid')):
+                _rpc_count_modules(port=self.exposed_port)
+                self.stop()
+                return
+        if self.is_running():
+            self.stop()
+            raise OdooTestTimeoutError('Odoo pid file never appeared after %s sec' % INSTALL_TIMEOUT)
+        raise OdooTestError('Error while installing/starting Odoo after %s sec.\nSee testlogs.txt in build dir' % int(time.time() - start_time))
+
+    def build(self):
+        """To be overriden by specific builder"""
+        pass
+
+    def start_test(self):
+        """To be overriden by specific builder"""
+        pass
+
+
+class DockerTgz(Docker):
+    """Docker class to build python src package"""
+
+    arch = 'tgz'
+
+    def build(self):
+        logging.info('Start building python tgz package')
+        self.run('python3 setup.py sdist --quiet --formats=gztar,zip', self.args.build_dir, 'odoo-src-build-%s' % TSTAMP)
+        os.rename(glob('%s/dist/odoo-*.tar.gz' % self.args.build_dir)[0], '%s/odoo_%s.%s.tar.gz' % (self.args.build_dir, VERSION, TSTAMP))
+        os.rename(glob('%s/dist/odoo-*.zip' % self.args.build_dir)[0], '%s/odoo_%s.%s.zip' % (self.args.build_dir, VERSION, TSTAMP))
+        logging.info('Finished building python tgz package')
+
+    def start_test(self):
+        if not self.args.test:
+            return
+        logging.info('Start testing python tgz package')
+        cmds = [
+            'service postgresql start',
+            'pip3 install /data/src/odoo_%s.%s.tar.gz' % (VERSION, TSTAMP),
+            'su postgres -s /bin/bash -c "createuser -s odoo"',
+            'su postgres -s /bin/bash -c "createdb mycompany"',
+            'su odoo -s /bin/bash -c "odoo -d mycompany -i base --stop-after-init"',
+            'su odoo -s /bin/bash -c "odoo -d mycompany --pidfile=/data/src/odoo.pid"',
+        ]
+        self.run(' && '.join(cmds), self.args.build_dir, 'odoo-src-test-%s' % TSTAMP, user='root', detach=True, exposed_port=8069, timeout=300)
+        self.test_odoo()
+        logging.info('Finished testing tgz package')
+
+
+class DockerDeb(Docker):
+    """Docker class to build debian package"""
+
+    arch = 'deb'
+
+    def build(self):
+        logging.info('Start building debian package')
+        # Append timestamp to version for the .dsc to refer the right .tar.gz
+        cmds = ["sed -i '1s/^.*$/odoo (%s.%s) stable; urgency=low/' debian/changelog" % (VERSION, TSTAMP)]
+        cmds.append('dpkg-buildpackage -rfakeroot -uc -us -tc')
+        # As the packages are built in the parent of the buildir, we move them back to build_dir
+        cmds.append('mv ../odoo_* ./')
+        self.run(' && '.join(cmds), self.args.build_dir, 'odoo-deb-build-%s' % TSTAMP)
+        logging.info('Finished building debian package')
+
+    def start_test(self):
+        if not self.args.test:
+            return
+        logging.info('Start testing debian package')
+        cmds = [
+            'service postgresql start',
+            'su postgres -s /bin/bash -c "createdb mycompany"',
+            '/usr/bin/apt-get update -y',
+            '/usr/bin/dpkg -i /data/src/odoo_%s.%s_all.deb ; /usr/bin/apt-get install -f -y' % (VERSION, TSTAMP),
+            'su odoo -s /bin/bash -c "odoo -d mycompany -i base --stop-after-init"',
+            'su odoo -s /bin/bash -c "odoo -d mycompany --pidfile=/data/src/odoo.pid"',
+        ]
+        self.run(' && '.join(cmds), self.args.build_dir, 'odoo-deb-test-%s' % TSTAMP, user='root', detach=True, exposed_port=8069, timeout=300)
+        self.test_odoo()
+        logging.info('Finished testing debian package')
+
+
+class DockerRpm(Docker):
+    """Docker class to build rpm package"""
+
+    arch = 'rpm'
+
+    def build(self):
+        logging.info('Start building fedora rpm package')
+        self.run('python3 setup.py --quiet bdist_rpm', self.args.build_dir, 'odoo-rpm-build-%s' % TSTAMP)
+        os.rename(glob('%s/dist/odoo-*.noarch.rpm' % self.args.build_dir)[0], '%s/odoo_%s.%s.rpm' % (self.args.build_dir, VERSION, TSTAMP))
+        logging.info('Finished building fedora rpm package')
+
+    def start_test(self):
+        if not self.args.test:
+            return
+        logging.info('Start testing rpm package')
+        cmds = [
+            'su postgres -c "/usr/bin/pg_ctl -D /var/lib/postgres/data start"',
+            'sleep 5',
+            'su postgres -c "createdb mycompany"',
+            'dnf install -d 0 -e 0 /data/src/odoo_%s.%s.rpm -y' % (VERSION, TSTAMP),
+            'su odoo -s /bin/bash -c "odoo -c /etc/odoo/odoo.conf -d mycompany -i base --stop-after-init"',
+            'su odoo -s /bin/bash -c "odoo -c /etc/odoo/odoo.conf -d mycompany --pidfile=/data/src/odoo.pid"',
+        ]
+        self.run(' && '.join(cmds), args.build_dir, 'odoo-rpm-test-%s' % TSTAMP, user='root', detach=True, exposed_port=8069, timeout=300)
+        self.test_odoo()
+        logging.info('Finished testing rpm package')
+
+
+# KVM stuffs
 class KVM(object):
-    def __init__(self, o, image, ssh_key='', login='openerp'):
-        self.o = o
-        self.image = image
-        self.ssh_key = ssh_key
-        self.login = login
+    def __init__(self, args):
+        self.args = args
+        self.image = args.vm_winxp_image
+        self.ssh_key = args.vm_winxp_ssh_key
+        self.login = args.vm_winxp_login
 
-    def timeout(self,signum,frame):
-        logging.warning("vm timeout kill (pid: {})".format(self.pid))
-        os.kill(self.pid,15)
+    def timeout(self, signum, frame):
+        logging.warning("vm timeout kill (pid: {})".format(self.kvm_proc.pid))
+        self.kvm_proc.terminate()
 
     def start(self):
-        l="kvm -cpu core2duo -smp 2,sockets=2,cores=1,threads=1 -net nic,model=rtl8139 -net user,hostfwd=tcp:127.0.0.1:10022-:22,hostfwd=tcp:127.0.0.1:18069-:8069,hostfwd=tcp:127.0.0.1:15432-:5432 -m 1024 -drive".split(" ")
-        #l.append('file=%s,if=virtio,index=0,boot=on,snapshot=on'%self.image)
-        l.append('file=%s,snapshot=on'%self.image)
-        #l.extend(['-vnc','127.0.0.1:1'])
-        l.append('-nographic')
-        logging.info("Starting kvm: {}".format( " ".join(l)))
-        self.pid=os.spawnvp(os.P_NOWAIT, l[0], l)
+        kvm_cmd = [
+            "kvm",
+            "-cpu", "core2duo",
+            "-smp", "2,sockets=2,cores=1,threads=1",
+            "-net", "nic,model=rtl8139",
+            "-net", "user,hostfwd=tcp:127.0.0.1:10022-:22,hostfwd=tcp:127.0.0.1:18069-:8069,hostfwd=tcp:127.0.0.1:15432-:5432",
+            "-m", "1024",
+            "-drive", "file=%s,snapshot=on" % self.image,
+            "-nographic"
+        ]
+        logging.info("Starting kvm: {}".format(" ".join(kvm_cmd)))
+        self.kvm_proc = subprocess.Popen(kvm_cmd)
         time.sleep(50)
         signal.alarm(2400)
         signal.signal(signal.SIGALRM, self.timeout)
@@ -202,335 +403,165 @@ class KVM(object):
             self.run()
         finally:
             signal.signal(signal.SIGALRM, signal.SIG_DFL)
-            os.kill(self.pid,15)
+            self.kvm_proc.terminate()
             time.sleep(10)
 
-    def ssh(self,cmd):
-        l=['ssh','-o','UserKnownHostsFile=/dev/null','-o','StrictHostKeyChecking=no','-p','10022','-i',self.ssh_key,'%s@127.0.0.1'%self.login,cmd]
-        system(l)
-
-    def rsync(self,args,options='--delete --exclude .git --exclude .tx --exclude __pycache__'):
-        cmd ='rsync -rt -e "ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -p 10022 -i %s" %s %s' % (self.ssh_key, options, args)
-        system(cmd)
+    def ssh(self, cmd):
+        run_cmd([
+            'ssh',
+            '-o', 'UserKnownHostsFile=/dev/null',
+            '-o', 'StrictHostKeyChecking=no',
+            '-p', '10022',
+            '-i', self.ssh_key,
+            '%s@127.0.0.1' % self.login,
+            cmd
+        ]).check_returncode()
+
+    def rsync(self, rsync_args, options=['--delete', '--exclude', '.git', '--exclude', '.tx', '--exclude', '__pycache__']):
+        cmd = [
+            'rsync',
+            '-rt',
+            '-e', 'ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -p 10022 -i %s' % self.ssh_key
+        ]
+        cmd.extend(options)
+        cmd.extend(rsync_args)
+        run_cmd(cmd).check_returncode()
 
     def run(self):
         pass
 
+
 class KVMWinBuildExe(KVM):
     def run(self):
-        with open(join(self.o.build_dir, 'setup/win32/Makefile.version'), 'w') as f:
-            f.write("VERSION=%s\n" % version.replace('~', '_').replace('+', ''))
-        with open(join(self.o.build_dir, 'setup/win32/Makefile.python'), 'w') as f:
-            f.write("PYTHON_VERSION=%s\n" % self.o.vm_winxp_python_version.replace('.', ''))
-        with open(join(self.o.build_dir, 'setup/win32/Makefile.servicename'), 'w') as f:
+        logging.info('Start building Windows package')
+        with open(os.path.join(self.args.build_dir, 'setup/win32/Makefile.version'), 'w') as f:
+            f.write("VERSION=%s\n" % VERSION.replace('~', '_').replace('+', ''))
+        with open(os.path.join(self.args.build_dir, 'setup/win32/Makefile.python'), 'w') as f:
+            f.write("PYTHON_VERSION=%s\n" % self.args.vm_winxp_python_version.replace('.', ''))
+        with open(os.path.join(self.args.build_dir, 'setup/win32/Makefile.servicename'), 'w') as f:
             f.write("SERVICENAME=%s\n" % nt_service_name)
 
         remote_build_dir = '/cygdrive/c/odoobuild/server/'
 
         self.ssh("mkdir -p build")
         logging.info("Syncing Odoo files to virtual machine...")
-        self.rsync('%s/ %s@127.0.0.1:%s' % (self.o.build_dir, self.login, remote_build_dir))
+        self.rsync(['%s/' % self.args.build_dir, '%s@127.0.0.1:%s' % (self.login, remote_build_dir)])
         self.ssh("cd {}setup/win32;time make allinone;".format(remote_build_dir))
-        self.rsync('%s@127.0.0.1:%ssetup/win32/release/ %s/' % (self.login, remote_build_dir, self.o.build_dir), '')
-        logging.info("KVMWinBuildExe.run(): done")
+        self.rsync(['%s@127.0.0.1:%ssetup/win32/release/' % (self.login, remote_build_dir), '%s/' % self.args.build_dir])
+        logging.info('Finished building Windows package')
+
 
 class KVMWinTestExe(KVM):
     def run(self):
-        setuppath = glob("%s/openerp-server-setup-*.exe" % self.o.build_dir)[0]
-        setupfile = setuppath.split('/')[-1]
+        logging.info('Start testing Windows package')
+        setup_path = glob("%s/openerp-server-setup-*.exe" % self.args.build_dir)[0]
+        setupfile = setup_path.split('/')[-1]
         setupversion = setupfile.split('openerp-server-setup-')[1].split('.exe')[0]
 
-        self.rsync('"%s" %s@127.0.0.1:' % (setuppath, self.login))
+        self.rsync(['"%s"' % setup_path, '%s@127.0.0.1:' % self.login])
         self.ssh("TEMP=/tmp ./%s /S" % setupfile)
         self.ssh('PGPASSWORD=openpgpwd /cygdrive/c/"Program Files"/"Odoo %s"/PostgreSQL/bin/createdb.exe -e -U openpg mycompany' % setupversion)
         self.ssh('netsh advfirewall set publicprofile state off')
         self.ssh('/cygdrive/c/"Program Files"/"Odoo {sv}"/python/python.exe \'c:\\Program Files\\Odoo {sv}\\server\\odoo-bin\' -d mycompany -i base --stop-after-init'.format(sv=setupversion))
         _rpc_count_modules(port=18069)
+        logging.info('Finished testing Windows package')
 
-#----------------------------------------------------------
-# Stage: building
-#----------------------------------------------------------
-def _prepare_build_dir(o, win32=False):
-    
-    cmd = ['rsync', '-a', '--exclude', '.git', '--exclude', '*.pyc', '--exclude', '*.pyo']
-    if not win32:
-        cmd += ['--exclude', 'setup/win32']
-    system(cmd + ['%s/' % o.odoo_dir, o.build_dir])
-    for addon_path in glob(join(o.build_dir, 'addons/*')):
-        if addon_path.split(os.path.sep)[-1] not in ADDONS_NOT_TO_PUBLISH:
-            try:
-                shutil.move(addon_path, join(o.build_dir, 'odoo/addons'))
-            except shutil.Error as e:
-                # Thrown when the add-on is already in odoo/addons (if _prepare_build_dir
-                # has already been called once)
-                logging.warning("Warning '{}' while moving addon '{}'".format(e,addon_path))
-                if addon_path.startswith(o.build_dir) and os.path.isdir(addon_path):
-                    logging.info("Removing '{}'".format(addon_path))
-                    try:
-                        shutil.rmtree(addon_path)
-                    except shutil.Error as rm_error:
-                        logging.warning("Cannot remove '{}': {}".format(addon_path, rm_error))
 
-def build_tgz(o):
-    system(['python3', 'setup.py', 'sdist', '--quiet', '--formats=gztar,zip'], o.build_dir)
-    system(['mv', glob('%s/dist/odoo-*.tar.gz' % o.build_dir)[0], '%s/odoo_%s.%s.tar.gz' % (o.build_dir, version, timestamp)])
-    system(['mv', glob('%s/dist/odoo-*.zip' % o.build_dir)[0], '%s/odoo_%s.%s.zip' % (o.build_dir, version, timestamp)])
-
-def build_deb(o):
-    # Append timestamp to version for the .dsc to refer the right .tar.gz
-    cmd=['sed', '-i', '1s/^.*$/odoo (%s.%s) stable; urgency=low/'%(version,timestamp), 'debian/changelog']
-    subprocess.call(cmd, cwd=o.build_dir)
-    if not o.no_debsign:
-        deb = pexpect.spawn('dpkg-buildpackage -rfakeroot -k%s' % GPGID, cwd=o.build_dir)
-        deb.logfile = stdout.buffer
-        if GPGPASSPHRASE:
-            deb.expect_exact('Enter passphrase: ', timeout=1200)
-            deb.send(GPGPASSPHRASE + '\r\n')
-            deb.expect_exact('Enter passphrase: ')
-            deb.send(GPGPASSPHRASE + '\r\n')
-        deb.expect(pexpect.EOF, timeout=1200)
-    else:
-        subprocess.call(['dpkg-buildpackage', '-rfakeroot', '-uc', '-us'], cwd=o.build_dir)
-    # As the packages are built in the parent of the buildir, we move them back to build_dir
-    build_dir_parent = '{}/../'.format(o.build_dir)
-    wildcards = ['odoo_{}'.format(wc) for wc in ('*.deb', '*.dsc', '*_amd64.changes', '*.tar.gz', '*.tar.xz')]
-    move_glob(build_dir_parent, wildcards, o.build_dir)
+def build_exe(args):
+    KVMWinBuildExe(args).start()
+    shutil.copy(glob('%s/openerp*.exe' % args.build_dir)[0], '%s/odoo_%s.%s.exe' % (args.build_dir, VERSION, TSTAMP))
 
-def build_rpm(o):
-    system(['python3', 'setup.py', '--quiet', 'bdist_rpm'], o.build_dir)
-    system(['mv', glob('%s/dist/odoo-*.noarch.rpm' % o.build_dir)[0], '%s/odoo_%s.%s.noarch.rpm' % (o.build_dir, version, timestamp)])
 
-def build_exe(o):
-    KVMWinBuildExe(o, o.vm_winxp_image, o.vm_winxp_ssh_key, o.vm_winxp_login).start()
-    system(['cp', glob('%s/openerp*.exe' % o.build_dir)[0], '%s/odoo_%s.%s.exe' % (o.build_dir, version, timestamp)])
+def test_exe(args):
+    if args.test:
+        KVMWinTestExe(args).start()
 
-#----------------------------------------------------------
-# Stage: testing
-#----------------------------------------------------------
-def _prepare_testing(o):
-    logging.info('Preparing testing')
-    if not o.no_tarball:
-        logging.info('Preparing docker container instance for tarball')
-        subprocess.call(["mkdir", "docker_src"], cwd=o.build_dir)
-        subprocess.call(["cp", "package.dfsrc", os.path.join(o.build_dir, "docker_src", "Dockerfile")],
-                        cwd=os.path.join(o.odoo_dir, "setup"))
-        # Use rsync to copy requirements.txt in order to keep original permissions
-        subprocess.call(["rsync", "-a", "requirements.txt", os.path.join(o.build_dir, "docker_src")],
-                        cwd=os.path.join(o.odoo_dir))
-        subprocess.call(["docker", "build", "-t", "odoo-%s-src-nightly-tests" % docker_version, "."],
-                        cwd=os.path.join(o.build_dir, "docker_src"))
-    if not o.no_debian:
-        logging.info('Preparing docker container instance for debian')
-        subprocess.call(["mkdir", "docker_debian"], cwd=o.build_dir)
-        subprocess.call(["cp", "package.dfdebian", os.path.join(o.build_dir, "docker_debian", "Dockerfile")],
-                        cwd=os.path.join(o.odoo_dir, "setup"))
-        # Use rsync to copy requirements.txt in order to keep original permissions
-        subprocess.call(["rsync", "-a", "requirements.txt", os.path.join(o.build_dir, "docker_debian")],
-                        cwd=os.path.join(o.odoo_dir))
-        subprocess.call(["docker", "build", "-t", "odoo-%s-debian-nightly-tests" % docker_version, "."],
-                        cwd=os.path.join(o.build_dir, "docker_debian"))
-    if not o.no_rpm:
-        logging.info('Preparing docker container instance for RPM') 
-        subprocess.call(["mkdir", "docker_fedora"], cwd=o.build_dir)
-        subprocess.call(["cp", "package.dffedora", os.path.join(o.build_dir, "docker_fedora", "Dockerfile")],
-                        cwd=os.path.join(o.odoo_dir, "setup"))
-        subprocess.call(["docker", "build", "-t", "odoo-%s-fedora-nightly-tests" % docker_version, "."],
-                        cwd=os.path.join(o.build_dir, "docker_fedora"))
-
-def test_tgz(o):
-    logging.info('Testing tarball in docker')
-    with docker('odoo-%s-src-nightly-tests' % docker_version, o.build_dir, o.pub) as wheezy:
-        wheezy.release = '*.tar.gz'
-        wheezy.system("service postgresql start")
-        wheezy.system('pip3 install /opt/release/%s' % wheezy.release)
-        wheezy.system("useradd --system --no-create-home odoo")
-        wheezy.system('su postgres -s /bin/bash -c "createuser -s odoo"')
-        wheezy.system('su postgres -s /bin/bash -c "createdb mycompany"')
-        wheezy.system('mkdir /var/lib/odoo')
-        wheezy.system('chown odoo:odoo /var/lib/odoo')
-        wheezy.system('su odoo -s /bin/bash -c "odoo -d mycompany -i base --stop-after-init"')
-        wheezy.system('su odoo -s /bin/bash -c "odoo -d mycompany &"')
-
-def test_deb(o):
-    logging.info('Testing deb package in docker')
-    with docker('odoo-%s-debian-nightly-tests' % docker_version, o.build_dir, o.pub) as wheezy:
-        wheezy.release = '*.deb'
-        wheezy.system("service postgresql start")
-        wheezy.system('su postgres -s /bin/bash -c "createdb mycompany"')
-        wheezy.system('/usr/bin/dpkg -i /opt/release/%s' % wheezy.release)
-        wheezy.system('/usr/bin/apt-get install -f -y')
-        wheezy.system('su odoo -s /bin/bash -c "odoo -c /etc/odoo/odoo.conf -d mycompany -i base --stop-after-init"')
-        wheezy.system('su odoo -s /bin/bash -c "odoo -c /etc/odoo/odoo.conf -d mycompany &"')
-
-def test_rpm(o):
-    logging.info('Testing rpm in docker')
-    with docker('odoo-%s-fedora-nightly-tests' % docker_version, o.build_dir, o.pub) as fedora24:
-        fedora24.release = '*.noarch.rpm'
-        # Start postgresql
-        fedora24.system('su postgres -c "/usr/bin/pg_ctl -D /var/lib/postgres/data start"')
-        fedora24.system('sleep 5')
-        fedora24.system('su postgres -c "createdb mycompany"')
-        # Odoo install
-        fedora24.system('dnf install -d 0 -e 0 /opt/release/%s -y' % fedora24.release)
-        fedora24.system('su odoo -s /bin/bash -c "odoo -c /etc/odoo/odoo.conf -d mycompany -i base --stop-after-init"')
-        fedora24.system('su odoo -s /bin/bash -c "odoo -c /etc/odoo/odoo.conf -d mycompany &"')
-
-def test_exe(o):
-    logging.info('Testng windows installer in KVM')
-    KVMWinTestExe(o, o.vm_winxp_image, o.vm_winxp_ssh_key, o.vm_winxp_login).start()
-
-#---------------------------------------------------------
-# Generates Packages, Sources and Release files of debian package
-#---------------------------------------------------------
-def gen_deb_package(o, published_files):
-    # Executes command to produce file_name in path, and moves it to o.pub/deb
-    def _gen_file(o, command, file_name, path):
-        cur_tmp_file_path = os.path.join(path, file_name)
-        with open(cur_tmp_file_path, 'w') as out:
-            subprocess.call(command, stdout=out, cwd=path)
-        system(['cp', cur_tmp_file_path, os.path.join(o.pub, 'deb', file_name)])
 
-    # Copy files to a temp directory (required because the working directory must contain only the
-    # files of the last release)
-    temp_path = tempfile.mkdtemp(suffix='debPackages')
-    for pub_file_path in published_files:
-        system(['cp', pub_file_path, temp_path])
+def parse_args():
+    ap = argparse.ArgumentParser()
+    build_dir = "%s-%s" % (ROOTDIR, TSTAMP)
+    log_levels = {"debug": logging.DEBUG, "info": logging.INFO, "warning": logging.WARN, "error": logging.ERROR, "critical": logging.CRITICAL}
 
-    commands = [
-        (['dpkg-scanpackages', '.'], "Packages"),  # Generate Packages file
-        (['dpkg-scansources', '.'], "Sources"),  # Generate Sources file
-        (['apt-ftparchive', 'release', '.'], "Release")  # Generate Release file
-    ]
-    # Generate files
-    for command in commands:
-        _gen_file(o, command[0], command[-1], temp_path)
-    # Remove temp directory
-    shutil.rmtree(temp_path)
+    ap.add_argument("-b", "--build-dir", default=build_dir, help="build directory (%(default)s)", metavar="DIR")
+    ap.add_argument("-p", "--pub", default=None, help="pub directory %(default)s", metavar="DIR")
+    ap.add_argument("--logging", action="store", choices=list(log_levels.keys()), default="info", help="Logging level")
+    ap.add_argument("--build-deb", action="store_true")
+    ap.add_argument("--build-rpm", action="store_true")
+    ap.add_argument("--build-tgz", action="store_true")
+    ap.add_argument("--build-win", action="store_true")
 
-    if not o.no_debsign:
-        # Generate Release.gpg (= signed Release)
-        # Options -abs: -a (Create ASCII armored output), -b (Make a detach signature), -s (Make a signature)
-        subprocess.call(['gpg', '--default-key', GPGID, '--passphrase', GPGPASSPHRASE, '--yes', '-abs', '--no-tty', '-o', 'Release.gpg', 'Release'], cwd=os.path.join(o.pub, 'deb'))
-
-#---------------------------------------------------------
-# Generates an RPM repo
-#---------------------------------------------------------
-def gen_rpm_repo(o, file_name):
-    # Sign the RPM
-    rpmsign = pexpect.spawn('/bin/bash', ['-c', 'rpm --resign %s' % file_name], cwd=os.path.join(o.pub, 'rpm'))
-    rpmsign.expect_exact('Enter pass phrase: ')
-    rpmsign.send(GPGPASSPHRASE + '\r\n')
-    rpmsign.expect(pexpect.EOF)
-
-    # Removes the old repodata
-    subprocess.call(['rm', '-rf', os.path.join(o.pub, 'rpm', 'repodata')])
-
-    # Copy files to a temp directory (required because the working directory must contain only the
-    # files of the last release)
-    temp_path = tempfile.mkdtemp(suffix='rpmPackages')
-    subprocess.call(['cp', file_name, temp_path])
-
-    subprocess.call(['createrepo', temp_path])  # creates a repodata folder in temp_path
-    subprocess.call(['cp', '-r', os.path.join(temp_path, "repodata"), os.path.join(o.pub, 'rpm')])
-
-    # Remove temp directory
-    shutil.rmtree(temp_path)
-
-#----------------------------------------------------------
-# Options and Main
-#----------------------------------------------------------
-def options():
-    op = optparse.OptionParser()
-    root = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
-    build_dir = "%s-%s" % (root, timestamp)
+    # Windows VM
+    ap.add_argument("--vm-winxp-image", default='/home/odoo/vm/win1036/win10_winpy36.qcow2', help="%(default)s")
+    ap.add_argument("--vm-winxp-ssh-key", default='/home/odoo/vm/win1036/id_rsa', help="%(default)s")
+    ap.add_argument("--vm-winxp-login", default='Naresh', help="Windows login %(default)s")
+    ap.add_argument("--vm-winxp-python-version", default='3.6', help="Windows Python version installed in the VM (default: %(default)s)")
 
-    log_levels = { "debug" : logging.DEBUG, "info": logging.INFO, "warning": logging.WARN, "error": logging.ERROR, "critical": logging.CRITICAL }
+    ap.add_argument("-t", "--test", action="store_true", default=False, help="Test built packages")
+    ap.add_argument("-s", "--sign", action="store_true", default=False, help="Sign Debian package / generate Rpm repo")
+    ap.add_argument("--no-remove", action="store_true", help="don't remove build dir")
+    ap.add_argument("--blacklist", nargs="*", help="Modules to blacklist in package")
 
-    op.add_option("-b", "--build-dir", default=build_dir, help="build directory (%default)", metavar="DIR")
-    op.add_option("-p", "--pub", default=None, help="pub directory (%default)", metavar="DIR")
-    op.add_option("", "--no-testing", action="store_true", help="don't test the built packages")
+    parsed_args = ap.parse_args()
+    logging.basicConfig(format='%(asctime)s %(levelname)s: %(message)s', datefmt='%Y-%m-%d %I:%M:%S', level=log_levels[parsed_args.logging])
+    parsed_args.odoo_dir = ROOTDIR
+    return parsed_args
 
-    op.add_option("", "--no-debian", action="store_true", help="don't build the debian package")
-    op.add_option("", "--no-debsign", action="store_true", help="don't sign the debian package")
-    op.add_option("", "--no-rpm", action="store_true", help="don't build the rpm package")
-    op.add_option("", "--no-tarball", action="store_true", help="don't build the tarball")
-    op.add_option("", "--no-windows", action="store_true", help="don't build the windows package")
 
-    # Windows VM
-    op.add_option("", "--vm-winxp-image", default='/home/odoo/vm/win1036/win10_winpy36.qcow2', help="%default")
-    op.add_option("", "--vm-winxp-ssh-key", default='/home/odoo/vm/win1036/id_rsa', help="%default")
-    op.add_option("", "--vm-winxp-login", default='Naresh', help="Windows login (%default)")
-    op.add_option("", "--vm-winxp-python-version", default='3.6', help="Windows Python version installed in the VM (default: %default)")
-    
-    op.add_option("", "--no-remove", action="store_true", help="don't remove build dir")
-    op.add_option("", "--logging", action="store", type="choice", choices=list(log_levels.keys()), default="info", help="Logging level")
-
-    (o, args) = op.parse_args()
-    logging.basicConfig(format='%(asctime)s %(levelname)s: %(message)s', datefmt='%Y-%m-%d %I:%M:%S', level=log_levels[o.logging])
-    # derive other options
-    o.odoo_dir = root
-    o.pkg = join(o.build_dir, 'pkg')
-    o.work = join(o.build_dir, 'openerp-%s' % version)
-    o.work_addons = join(o.work, 'odoo', 'addons')
-
-    return o
-
-def main():
-    o = options()
-    _prepare_build_dir(o)
-    if not o.no_testing:
-        _prepare_testing(o)
+def main(args):
     try:
-        if not o.no_tarball:
-            build_tgz(o)
+        if args.build_tgz:
+            _prepare_build_dir(args)
+            docker_tgz = DockerTgz(args)
+            docker_tgz.build()
             try:
-                if not o.no_testing:
-                    test_tgz(o)
-                published_files = publish(o, 'tarball', ['tar.gz', 'zip'])
+                docker_tgz.start_test()
+                published_files = publish(args, 'tgz', ['tar.gz', 'zip'])
             except Exception as e:
                 logging.error("Won't publish the tgz release.\n Exception: %s" % str(e))
-        if not o.no_debian:
-            build_deb(o)
+        if args.build_rpm:
+            _prepare_build_dir(args)
+            docker_rpm = DockerRpm(args)
+            docker_rpm.build()
             try:
-                if not o.no_testing:
-                    test_deb(o)
-                published_files = publish(o, 'debian', ['deb', 'dsc', 'changes', 'tar.xz'])
-                gen_deb_package(o, published_files)
+                docker_rpm.start_test()
+                published_files = publish(args, 'rpm', ['rpm'])
+                if args.sign:
+                    gen_rpm_repo(args, published_files[0])
             except Exception as e:
-                logging.error("Won't publish the deb release.\n Exception: %s" % str(e))
-                traceback.print_exc()
-        if not o.no_rpm:
-            build_rpm(o)
+                logging.error("Won't publish the rpm release.\n Exception: %s" % str(e))
+        if args.build_deb:
+            _prepare_build_dir(args)
+            docker_deb = DockerDeb(args)
+            docker_deb.build()
             try:
-                if not o.no_testing:
-                    test_rpm(o)
-                published_files = publish(o, 'redhat', ['noarch.rpm'])
-                gen_rpm_repo(o, published_files[0])
+                docker_deb.start_test()
+                published_files = publish(args, 'deb', ['deb', 'dsc', 'changes', 'tar.xz'])
+                gen_deb_package(args, published_files)
             except Exception as e:
-                logging.error("Won't publish the rpm release.\n Exception: %s" % str(e))
-        if not o.no_windows:
-            _prepare_build_dir(o, win32=True)
-            build_exe(o)
+                logging.error("Won't publish the deb release.\n Exception: %s" % str(e))
+        if args.build_win:
+            _prepare_build_dir(args, win32=True)
+            build_exe(args)
             try:
-                if not o.no_testing:
-                    test_exe(o)
-                published_files = publish(o, 'windows', ['exe'])
+                test_exe(args)
+                published_files = publish(args, 'windows', ['exe'])
             except Exception as e:
                 logging.error("Won't publish the exe release.\n Exception: %s" % str(e))
     except Exception as e:
         logging.error('Something bad happened ! : {}'.format(e))
         traceback.print_exc()
     finally:
-        if o.no_remove:
-            logging.info('Build dir "{}" not removed'.format(o.build_dir))
+        if args.no_remove:
+            logging.info('Build dir "{}" not removed'.format(args.build_dir))
         else:
-            shutil.rmtree(o.build_dir)
-            logging.info('Build dir %s removed' % o.build_dir)
-
-        if not o.no_testing and not (o.no_debian and o.no_rpm and o.no_tarball):
-            system("docker rm -f `docker ps -a | awk '{print $1 }'` 2>>/dev/null")
-            logging.info('Remaining dockers removed')
+            if os.path.exists(args.build_dir):
+                shutil.rmtree(args.build_dir)
+                logging.info('Build dir %s removed' % args.build_dir)
 
 
 if __name__ == '__main__':
-    main()
+    args = parse_args()
+    if os.path.exists(args.build_dir):
+        logging.error('Build dir "%s" already exists.', args.build_dir)
+        sys.exit(1)
+    main(args)
diff --git a/setup/redhat/install.sh b/setup/redhat/install.sh
index 982cc8759e99..b7a7f3eb40df 100644
--- a/setup/redhat/install.sh
+++ b/setup/redhat/install.sh
@@ -1,3 +1,3 @@
 #!/bin/sh
 set -e
-python3 setup.py install --prefix=/usr --root=$RPM_BUILD_ROOT --record=INSTALLED_FILES --install-lib usr/lib/python3.6/site-packages/
+python3 setup.py install --prefix=/usr --root=$RPM_BUILD_ROOT --record=INSTALLED_FILES --install-lib usr/lib/python3.7/site-packages/
diff --git a/setup/redhat/postinstall.sh b/setup/redhat/postinstall.sh
index e6ee9106f7d7..c14dd9a55a4e 100644
--- a/setup/redhat/postinstall.sh
+++ b/setup/redhat/postinstall.sh
@@ -10,12 +10,6 @@ ODOO_LOG_DIR=/var/log/odoo
 ODOO_LOG_FILE=$ODOO_LOG_DIR/odoo-server.log
 ODOO_USER="odoo"
 
-if [ -d /usr/lib/python3.7 ]; then
-    SITE_PACK_DIR37=/usr/lib/python3.7/site-packages
-    [[ ! -d ${SITE_PACK_DIR37} ]] && mkdir -p ${SITE_PACK_DIR37}
-    ln -s /usr/lib/python3.6/site-packages/odoo ${SITE_PACK_DIR37}/odoo
-fi
-
 if ! getent passwd | grep -q "^odoo:"; then
     groupadd $ODOO_GROUP
     adduser --system --no-create-home $ODOO_USER -g $ODOO_GROUP
@@ -34,7 +28,7 @@ db_host = False
 db_port = False
 db_user = $ODOO_USER
 db_password = False
-addons_path = /usr/lib/python3.6/site-packages/odoo/addons
+addons_path = /usr/lib/python3.7/site-packages/odoo/addons
 " > $ODOO_CONFIGURATION_FILE
     chown $ODOO_USER:$ODOO_GROUP $ODOO_CONFIGURATION_FILE
     chmod 0640 $ODOO_CONFIGURATION_FILE
-- 
GitLab