Skip to content
Snippets Groups Projects
  • Maxim Cournoyer's avatar
    80c81ed2
    build.py: Really run in GDB when using --debug. · 80c81ed2
    Maxim Cournoyer authored
    The usage (--help) says: "Build with debug support; run in GDB", but
    GDB was only used when using the D-Bus daemon.  Run a libwrap built
    Jami with GDB as well.
    
    * build.py (run_run): Disable SIGINT when using GDB, and invoke the
    Jami client with GDB.  Disable output redirection to logs when using GDB.
    
    Change-Id: Icf3415a3d1fbb87f193d0ede07cb1e1fbb179ce2
    80c81ed2
    History
    build.py: Really run in GDB when using --debug.
    Maxim Cournoyer authored
    The usage (--help) says: "Build with debug support; run in GDB", but
    GDB was only used when using the D-Bus daemon.  Run a libwrap built
    Jami with GDB as well.
    
    * build.py (run_run): Disable SIGINT when using GDB, and invoke the
    Jami client with GDB.  Disable output redirection to logs when using GDB.
    
    Change-Id: Icf3415a3d1fbb87f193d0ede07cb1e1fbb179ce2
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
build.py 24.75 KiB
#!/usr/bin/env python3
# build.py --- Convenience script for building and running Jami

# Copyright (C) 2016-2023 Savoir-faire Linux Inc.
#
# 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 3 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 argparse
import contextlib
import multiprocessing
import os
import platform
import shlex
import signal
import shutil
import subprocess
import sys
import time

OSX_DISTRIBUTION_NAME = "osx"
WIN32_DISTRIBUTION_NAME = "win32"

# vs vars
win_sdk_default = '10.0.18362.0'

APT_BASED_DISTROS = [
    'debian',
    'linuxmint',
    'raspbian',
    'trisquel',
    'ubuntu',
]

DNF_BASED_DISTROS = [
    'fedora', 'rhel',
]

PACMAN_BASED_DISTROS = [
    'arch', 'parabola',
]

ZYPPER_BASED_DISTROS = [
    'opensuse-leap',
]

FLATPAK_BASED_RUNTIMES = [
    'org.gnome.Platform',
]

APT_INSTALL_SCRIPT = [
    'apt-get update',
    'apt-get install %(packages)s'
]

BREW_UNLINK_SCRIPT = [
    'brew unlink %(packages)s'
]

BREW_INSTALL_SCRIPT = [
    'brew update',
    'brew install %(packages)s',
    'brew link --force --overwrite %(packages)s'
]

RPM_INSTALL_SCRIPT = [
    'dnf update',
    'dnf install %(packages)s'
]

PACMAN_INSTALL_SCRIPT = [
    'pacman -Sy',
    'pacman -S --asdeps --needed %(packages)s'
]

ZYPPER_INSTALL_SCRIPT = [
    'zypper update',
    'zypper install %(packages)s'
]

ZYPPER_DEPENDENCIES = [
    # build system
    'autoconf', 'autoconf-archive', 'automake', 'cmake', 'make', 'patch', 'gcc-c++',
    'libtool', 'which', 'pandoc', 'nasm', 'doxygen', 'graphviz',
    # contrib dependencies
    'curl', 'gzip', 'bzip2',
    # daemon
    'speexdsp-devel', 'speex-devel', 'libdbus-c++-devel', 'jsoncpp-devel', 'yaml-cpp-devel',
    'yasm', 'libuuid-devel', 'libnettle-devel', 'libopus-devel', 'libexpat-devel',
    'libgnutls-devel', 'msgpack-devel', 'libavcodec-devel', 'libavdevice-devel', 'pcre-devel',
    'alsa-devel', 'libpulse-devel', 'libudev-devel', 'libva-devel', 'libvdpau-devel',
    'libopenssl-devel', 'libavutil-devel',
]

ZYPPER_CLIENT_DEPENDENCIES = [
    # lrc
    'qt6-core-devel', 'qt6-dbus-devel', 'qt6-linguist-devel',
    # client-qt
    'qt6-svg-devel', 'qt6-multimedia-devel', 'qt6-declarative-devel',
    'qt6-quickcontrols2-devel',
    'qrencode-devel', 'NetworkManager-devel'
]

ZYPPER_QT_WEBENGINE = [
    'qt6-webenginecore-devel',
    'qt6-webenginequick-devel',
    'qt6-webenginewidgets-devel'
]

DNF_DEPENDENCIES = [
    'autoconf', 'autoconf-archive', 'automake', 'cmake', 'make', 'speexdsp-devel', 'pulseaudio-libs-devel',
    'libtool', 'dbus-devel', 'expat-devel', 'pcre-devel', 'doxygen', 'graphviz',
    'yaml-cpp-devel', 'boost-devel', 'dbus-c++-devel', 'dbus-devel',
    'libXext-devel', 'libXfixes-devel', 'yasm',
    'speex-devel', 'chrpath', 'check', 'astyle', 'uuid-c++-devel', 'gettext-devel',
    'gcc-c++', 'which', 'alsa-lib-devel', 'systemd-devel', 'libuuid-devel',
    'uuid-devel', 'gnutls-devel', 'nettle-devel', 'opus-devel', 'speexdsp-devel',
    'yaml-cpp-devel', 'swig', 'jsoncpp-devel',
    'patch', 'libva-devel', 'openssl-devel', 'libvdpau-devel', 'msgpack-devel',
    'sqlite-devel', 'openssl-static', 'pandoc', 'nasm',
    'bzip2'
]

DNF_CLIENT_DEPENDENCIES = [
    'libnotify-devel',
    'qt6-qtbase-devel',
    'qt6-qtsvg-devel', 'qt6-qtmultimedia-devel', 'qt6-qtdeclarative-devel',
    'qrencode-devel', 'NetworkManager-libnm-devel'
]

DNF_QT_WEBENGINE = ['qt6-qtwebengine-devel']

APT_DEPENDENCIES = [
    'autoconf', 'autoconf-archive', 'autopoint', 'automake', 'cmake', 'make', 'dbus', 'doxygen', 'graphviz',
    'g++', 'gettext', 'libasound2-dev', 'libavcodec-dev',
    'libavdevice-dev', 'libavformat-dev', 'libboost-dev',
    'libcppunit-dev', 'libdbus-1-dev',
    'libdbus-c++-dev', 'libebook1.2-dev', 'libexpat1-dev', 'libgnutls28-dev',
    'libgtk-3-dev', 'libjack-dev',
    'libopus-dev', 'libpcre3-dev', 'libpulse-dev', 'libssl-dev',
    'libspeex-dev', 'libspeexdsp-dev', 'libswscale-dev', 'libtool',
    'libudev-dev', 'libyaml-cpp-dev', 'sip-tester', 'swig',
    'uuid-dev', 'yasm', 'libjsoncpp-dev', 'libva-dev', 'libvdpau-dev', 'libmsgpack-dev',
    'pandoc', 'nasm', 'dpkg-dev'
]

APT_CLIENT_DEPENDENCIES = [
    'qt6-base-dev', 'qt6-tools-dev', 'qt6-tools-dev-tools',
    'qt6-l10n-tools', 'libnotify-dev', 'libqt6sql6-sqlite',
    'libqt6core5compat6-dev', 'libqt6networkauth6-dev',
    'qt6-multimedia-dev', 'libqt6svg6-dev', 'qt6-declarative-dev',
    'qml6-module-qt-labs-qmlmodels',
    'qml6-module-qt5compat-graphicaleffects',
    'qml6-module-qtqml-workerscript',
    'qml6-module-qtmultimedia',
    'qml6-module-qtquick', 'qml6-module-qtquick-controls',
    'qml6-module-qtquick-dialogs', 'qml6-module-qtquick-layouts',
    'qml6-module-qtquick-shapes', 'qml6-module-qtquick-window',
    'qml6-module-qtquick-templates', 'qml6-module-qt-labs-platform',
    'libqrencode-dev', 'libnm-dev'
]

APT_QT_WEBENGINE = [
    'libqt6webengine6-data', 'libqt6webenginecore6-bin',
    'qt6-webengine-dev', 'qt6-webengine-dev-tools',
    'qml6-module-qtwebengine', 'qml6-module-qtwebchannel']

PACMAN_DEPENDENCIES = [
    'autoconf', 'autoconf-archive', 'gettext', 'cmake', 'dbus', 'doxygen', 'graphviz',
    'gcc', 'ffmpeg', 'boost', 'cppunit', 'libdbus', 'dbus-c++', 'libe-book', 'expat',
    'jack', 'opus', 'pcre', 'libpulse', 'speex', 'speexdsp', 'libtool', 'yaml-cpp',
    'swig', 'yasm', 'make', 'patch', 'pkg-config',
    'automake', 'libva', 'libvdpau', 'openssl', 'pandoc', 'nasm'
]

PACMAN_CLIENT_DEPENDENCIES = [
    # lrc
    'qt6-base',
    # client-qt
    'qt6-declarative', 'qt6-5compat', 'qt6-multimedia',
    'qt6-networkauth', 'qt6-shadertools',
    'qt6-svg', 'qt6-tools',
    'qrencode', 'libnm'
]

PACMAN_QT_WEBENGINE = ['qt6-webengine']

OSX_DEPENDENCIES = [
    'autoconf', 'cmake', 'gettext', 'pkg-config', 'qt6',
    'libtool', 'yasm', 'nasm', 'automake'
]

OSX_DEPENDENCIES_UNLINK = [
    'autoconf*', 'cmake*', 'gettext*', 'pkg-config*', 'qt*', 'qt@6.*',
    'libtool*', 'yasm*', 'nasm*', 'automake*', 'gnutls*', 'nettle*', 'msgpack*'
]

UNINSTALL_DAEMON_SCRIPT = [
    'make -C daemon uninstall'
]

ASSUME_YES_FLAG = ' -y'
ASSUME_YES_FLAG_PACMAN = ' --noconfirm'

GUIX_MANIFEST = 'extras/packaging/gnu-linux/guix/manifest.scm'


def run_powershell_cmd(cmd):
    p = subprocess.Popen(["powershell.exe", cmd], stdout=sys.stdout)
    p.communicate()
    p.wait()
    return


def run_dependencies(args):
    if args.distribution == WIN32_DISTRIBUTION_NAME:
        run_powershell_cmd(
            'Set-ExecutionPolicy Unrestricted; .\\extras\\scripts\\install-deps-windows.ps1')

    elif args.distribution in APT_BASED_DISTROS:
        if args.assume_yes:
            for i, _ in enumerate(APT_INSTALL_SCRIPT):
                APT_INSTALL_SCRIPT[i] += ASSUME_YES_FLAG
        execute_script(
            APT_INSTALL_SCRIPT,
            {"packages": ' '.join(map(shlex.quote, APT_DEPENDENCIES))})
        if not args.no_webengine:
            APT_CLIENT_DEPENDENCIES.extend(APT_QT_WEBENGINE)
        execute_script(
            APT_INSTALL_SCRIPT,
            {"packages": ' '.join(map(shlex.quote, APT_CLIENT_DEPENDENCIES))})

    elif args.distribution in DNF_BASED_DISTROS:
        if args.assume_yes:
            for i, _ in enumerate(DNF_INSTALL_SCRIPT):
                DNF_INSTALL_SCRIPT[i] += ASSUME_YES_FLAG
        execute_script(
            RPM_INSTALL_SCRIPT,
            {"packages": ' '.join(map(shlex.quote, DNF_DEPENDENCIES))})
        if not args.no_webengine:
            DNF_CLIENT_DEPENDENCIES.extend(DNF_QT_WEBENGINE)
        execute_script(
            RPM_INSTALL_SCRIPT,
            {"packages": ' '.join(map(shlex.quote, DNF_CLIENT_DEPENDENCIES))})

    elif args.distribution in PACMAN_BASED_DISTROS:
        if args.assume_yes:
            for i, _ in enumerate(PACMAN_INSTALL_SCRIPT):
                PACMAN_INSTALL_SCRIPT[i] += ASSUME_YES_FLAG_PACMAN
        execute_script(
            PACMAN_INSTALL_SCRIPT,
            {"packages": ' '.join(map(shlex.quote, PACMAN_DEPENDENCIES))})
        if not args.no_webengine:
            PACMAN_CLIENT_DEPENDENCIES.extend(PACMAN_QT_WEBENGINE)
        execute_script(
            PACMAN_INSTALL_SCRIPT,
            {"packages": ' '.join(map(shlex.quote, PACMAN_CLIENT_DEPENDENCIES))})

    elif args.distribution in ZYPPER_BASED_DISTROS:
        if args.assume_yes:
            for i, _ in enumerate(ZYPPER_INSTALL_SCRIPT):
                ZYPPER_INSTALL_SCRIPT[i] += ASSUME_YES_FLAG
        execute_script(
            ZYPPER_INSTALL_SCRIPT,
            {"packages": ' '.join(map(shlex.quote, ZYPPER_DEPENDENCIES))})
        if not args.no_webengine:
            ZYPPER_CLIENT_DEPENDENCIES.extend(ZYPPER_QT_WEBENGINE)
        execute_script(
            ZYPPER_INSTALL_SCRIPT,
            {"packages": ' '.join(map(shlex.quote, ZYPPER_CLIENT_DEPENDENCIES))})

    elif args.distribution == OSX_DISTRIBUTION_NAME:
        execute_script(
            BREW_UNLINK_SCRIPT,
            {"packages": ' '.join(map(shlex.quote, OSX_DEPENDENCIES_UNLINK))},
            False
        )
        execute_script(
            BREW_INSTALL_SCRIPT,
            {"packages": ' '.join(map(shlex.quote, OSX_DEPENDENCIES))},
            False
        )

    elif args.distribution == WIN32_DISTRIBUTION_NAME:
        print("The win32 version does not install dependencies with this script.\nPlease continue with the --install instruction.")
        sys.exit(1)
    elif args.distribution == 'guix':
        print(f"Building the profile defined in '{GUIX_MANIFEST}'...")
        execute_script([f'guix shell --manifest={GUIX_MANIFEST} -- true'])

    else:
        print("Not yet implemented for current distribution (%s). Please continue with the --install instruction. Note: You may need to install some dependencies manually." %
              args.distribution)
        sys.exit(1)


def run_init(args):
    """Initialize the git submodules and install the commit-msg hook."""
    subprocess.run(["git", "submodule", "update", "--init"],
                   check=True)

    client_hooks_dir = '.git/hooks'
    daemon_hooks_dir = '.git/modules/daemon/hooks'

    print("Installing commit-msg hooks...")
    # Copy the commit-msg hook to all modules in the same way.
    for hooks_dir in [client_hooks_dir, daemon_hooks_dir]:
        if not os.path.exists(hooks_dir):
            os.makedirs(hooks_dir)
        copy_file("./extras/scripts/commit-msg",
                  os.path.join(hooks_dir, "commit-msg"))

    print("Installing pre-commit hooks...")
    format_script = "./extras/scripts/format.py"
    # Prepend with the python executable if on Windows (not WSL).
    if sys.platform == 'win32':
        format_script = f'python {format_script}'
    # The client submodule has QML files, so we need to run qmlformat on it,
    # and thus need to supply the Qt path.
    execute_script([f'{format_script} --install {client_hooks_dir}'
                    f' --qt {args.qt}' if args.qt else ''],
                   {"path": client_hooks_dir})

    # The daemon submodule has no QML files, so we don't need to run
    # qmlformat on it, and thus don't need to supply the Qt path.
    execute_script([f'{format_script} --install {daemon_hooks_dir}'],
                   {"path": daemon_hooks_dir})


def copy_file(src, dest):
    print(f'Copying: {src} to {dest}')
    try:
        shutil.copy2(src, dest)
    # e.g. src and dest are the same file
    except shutil.Error as e:
        print(f'Error: {e}')
    # e.g. source or destination doesn't exist
    except IOError as e:
        print(f'Error: {e.strerror}')


@contextlib.contextmanager
def cwd(path):
    owd = os.getcwd()
    os.chdir(path)
    try:
        yield
    finally:
        os.chdir(owd)


def run_install(args):
    # Platforms with special compilation scripts
    if args.distribution == WIN32_DISTRIBUTION_NAME:
        with cwd('daemon/compat/msvc'):
            execute_script(
                ['python winmake.py -iv '
                 f'-s {args.sdk} -b daemon'])
        build_windows = 'extras/scripts/build-windows.py'
        execute_script([f'python {build_windows} --init'])
        execute_script([f'python {build_windows} --qt={args.qt}'])
        return True

    # Unix-like platforms
    environ = os.environ.copy()

    install_args = ['-p', str(multiprocessing.cpu_count())]
    if args.static:
        install_args.append('-s')
    if args.global_install:
        install_args.append('-g')
    if args.prefix:
        install_args += ('-P', args.prefix)
    if not args.priv_install:
        install_args.append('-u')
    if args.debug:
        install_args.append('-d')
    if args.no_libwrap:
        install_args.append('-W')
    if args.no_webengine:
        install_args.append('-w')
    if args.arch:
        install_args += ('-a', args.arch)

    if args.distribution == OSX_DISTRIBUTION_NAME:
        # The `universal_newlines` parameter has been renamed to `text` in
        # Python 3.7+ and triggering automatical binary to text conversion is
        # what it actually does
        proc = subprocess.run(["brew", "--prefix", "qt6"],
                              stdout=subprocess.PIPE, check=True,
                              universal_newlines=True)

        environ['CMAKE_PREFIX_PATH'] = proc.stdout.rstrip("\n")
        environ['CONFIGURE_FLAGS'] = '--without-dbus'
        if not args.qt:
            raise Exception(
                'provide the Qt path using --qt=/qt/install/prefix')
        install_args += ("-Q", args.qt)
    else:
        if args.distribution in ZYPPER_BASED_DISTROS:
            # fix jsoncpp pkg-config bug, remove when jsoncpp package bumped
            environ['JSONCPP_LIBS'] = "-ljsoncpp"
        if args.qt:
            install_args += ("-Q", args.qt)

    command = ['extras/scripts/install.sh'] + install_args

    if args.distribution == 'guix':
        if args.global_install:
            print('error: global install is not supported when using Guix.')
            sys.exit(1)
        # Run the build in an isolated container.
        share_tarballs_args = []
        if 'TARBALLS' in os.environ:
            share_tarballs_args = ['--preserve=TARBALLS',
                                   f'--share={os.environ["TARBALLS"]}']
        else:
            print('info: consider setting the TARBALLS environment variable '
                  'to a stable writable location to avoid loosing '
                  'cached tarballs')
        command = ['guix', 'shell', f'--manifest={GUIX_MANIFEST}',
                   '--symlink=/usr/bin/env=bin/env',
                   '--symlink=/etc/ssl/certs=etc/ssl/certs',
                   '--container', '--network'] + share_tarballs_args \
            + ['--'] + command

    print(f'info: Building/installing using the command: {" ".join(command)}')
    return subprocess.run(command, env=environ, check=True)


def run_uninstall(args):
    execute_script(UNINSTALL_DAEMON_SCRIPT)

    BUILD_DIR = 'build-global' if args.global_install else 'build'

    if (os.path.exists(BUILD_DIR)):
        UNINSTALL_CLIENT_SCRIPT = [
            f'make -C {BUILD_DIR} uninstall',
            f'rm -rf {BUILD_DIR}'
        ]
        execute_script(UNINSTALL_CLIENT_SCRIPT)


def run_clean():
    execute_script(['git clean -xfd',
                    'git submodule foreach git clean -xfd'])


def run_run(args):
    run_env = os.environ

    if args.debug:
        # Ignore the interruption signal when using GDB, as it's
        # common to use C-c when debugging and we do not want the
        # Python script to abort the debugging session.
        signal.signal(signal.SIGINT, signal.SIG_IGN)

    try:
        if args.no_libwrap:
            jamid_log = open("daemon.log", 'a')
            jamid_log.write('=== Starting daemon (%s) ===' %
                            time.strftime("%d/%m/%Y %H:%M:%S"))
            jamid_process = subprocess.Popen(
                ["./install/libexec/jamid", "-c", "-d"],
                stdout=jamid_log,
                stderr=jamid_log)

            with open('daemon.pid', 'w') as f:
                f.write(str(jamid_process.pid)+'\n')

        client_log = open('jami.log', 'a')
        client_log.write('=== Starting client (%s) ===' %
                         time.strftime("%d/%m/%Y %H:%M:%S"))
        jami_cmdline = ['install/bin/jami', '-d']
        if args.debug:
            jami_cmdline = ['gdb', '-ex', 'run', '--args'] + jami_cmdline

        print('Invoking jami with: {}'.format(str.join(' ', jami_cmdline)))
        if args.debug:
            print('Debugging with GDB; NOT redirecting output to log file')
        client_process = subprocess.Popen(
            jami_cmdline,
            stdout=False if args.debug else client_log,
            stderr=False if args.debug else client_log,
            env=run_env)

        with open('jami.pid', 'w') as f:
            f.write(str(client_process.pid)+'\n')

        if args.debug and args.no_libwrap:
            subprocess.call(['gdb', './install/libexec/jamid'])

        if not args.background:
            if args.no_libwrap:
                jamid_process.wait()
            client_process.wait()

    except KeyboardInterrupt:
        print("\nCaught KeyboardInterrupt...")

    finally:
        if args.debug:
            # Restore the default signal handler for SIGINT.
            signal.signal(signal.SIGINT, signal.SIG_DFL)
        if not args.background:
            try:
                # Only kill the processes if they are running, as they
                # could have been closed by the user.
                print("Killing processes...")
                if args.no_libwrap:
                    jamid_log.close()
                    if jamid_process.poll() is None:
                        jamid_process.kill()
                client_log.close()
                if client_process.poll() is None:
                    client_process.kill()
            except UnboundLocalError:
                # Its okay! We crashed before we could start a process
                # or open a file.  All that matters is that we close
                # files and kill processes in the right order.
                pass
    return True


def run_stop(args):
    STOP_SCRIPT = ['xargs kill < jami.pid',
                   'xargs kill < daemon.pid']
    execute_script(STOP_SCRIPT)


def execute_script(script, settings=None, fail=True):
    if settings is None:
        settings = {}
    for line in script:
        line = line % settings
        rv = os.system(line)
        if rv and fail:
            print('Error executing script! Exit code: %s (%s)' %
                  (rv, script), file=sys.stderr)
            sys.exit(1)


def has_guix():
    """Check whether the 'guix' command is available."""
    with open(os.devnull, 'w') as f:
        try:
            subprocess.run(["sh", "-c", "command -v guix"],
                           check=True, stdout=f)
        except subprocess.CalledProcessError:
            return False
        else:
            return True


def validate_args(parsed_args):
    """Validate the args values, exit if error is found"""

    # Filter unsupported distributions.
    supported_distros = \
        [OSX_DISTRIBUTION_NAME, WIN32_DISTRIBUTION_NAME, 'guix'] + \
        APT_BASED_DISTROS + DNF_BASED_DISTROS + PACMAN_BASED_DISTROS \
        + ZYPPER_BASED_DISTROS + FLATPAK_BASED_RUNTIMES

    if (parsed_args.distribution == 'no-check'
            or 'JAMI_BUILD_NO_CHECK' in os.environ):
        return

    if parsed_args.distribution not in supported_distros:
        print(f'WARNING: Distribution \'{parsed_args.distribution}\' is not '
              f'supported. Choose one of: {", ".join(supported_distros)}. '
              'Alternatively, you may force execution of this script '
              'by providing the \'--distribution=no-check\' argument or by '
              'exporting the JAMI_BUILD_NO_CHECK environment variable.',
              file=sys.stderr)
        sys.exit(1)

    # On Windows, version 10 or later is needed to build Jami.
    if parsed_args.distribution == WIN32_DISTRIBUTION_NAME:
        if hasattr(sys, 'getwindowsversion') and sys.getwindowsversion()[0] < 10:
            print('Windows 10 or later is needed to build Jami')
            sys.exit(1)


def parse_args():
    ap = argparse.ArgumentParser(description="Jami build tool")

    ga = ap.add_mutually_exclusive_group(required=True)
    ga.add_argument(
        '--init', action='store_true',
        help='Init Jami repository')
    ga.add_argument(
        '--dependencies', action='store_true',
        help='Install Jami build dependencies')
    ga.add_argument(
        '--install', action='store_true',
        help='Build and install Jami')
    ga.add_argument(
        '--clean', action='store_true',
        help='Call "git clean" on every repository of the project'
    )
    ga.add_argument(
        '--uninstall', action='store_true',
        help='Uninstall Jami')
    ga.add_argument(
        '--run', action='store_true',
        help='Run the Jami daemon and client')
    ga.add_argument(
        '--stop', action='store_true',
        help='Stop the Jami processes')

    ap.add_argument('--distribution')
    ap.add_argument('--prefix')
    ap.add_argument('--static', default=False, action='store_true')
    ap.add_argument('--global-install', default=False, action='store_true')
    ap.add_argument('--debug', default=False, action='store_true',
                    help='Build with debug support; run in GDB')
    ap.add_argument('--background', default=False, action='store_true')
    ap.add_argument('--no-priv-install', dest='priv_install',
                    default=True, action='store_false')
    ap.add_argument('--qt', type=str,
                    help='Use the Qt path supplied')
    ap.add_argument('--no-libwrap', dest='no_libwrap',
                    default=False, action='store_true',
                    help='Disable libwrap. Also set --disable-shared option to daemon configure')
    ap.add_argument('-y', '--assume-yes', default=False, action='store_true',
                    help='Assume yes (do not prompt user) for dependency installations through the system package manager')
    ap.add_argument('--no-webengine', dest='no_webengine',
                    default=False, action='store_true',
                    help='Do not use Qt WebEngine.')
    ap.add_argument('--arch')

    dist = choose_distribution()

    if dist == WIN32_DISTRIBUTION_NAME:
        ap.add_argument('--sdk', default=win_sdk_default, type=str,
                        help='Windows use only, specify Windows SDK version')

    parsed_args = ap.parse_args()

    if parsed_args.distribution:
        parsed_args.distribution = parsed_args.distribution.lower()
    else:
        parsed_args.distribution = dist

    validate_args(parsed_args)

    return parsed_args


def choose_distribution():
    system = platform.system().lower()

    if system == "linux" or system == "linux2":
        if os.path.isfile("/etc/arch-release"):
            return "arch"
        try:
            with open("/etc/os-release") as f:
                for line in f:
                    k, v = line.split("=")
                    if k.strip() == 'ID':
                        return v.strip().replace('"', '').split(' ')[0]
        except FileNotFoundError:
            if has_guix():
                return 'guix'
            return 'Unknown'
    elif system == "darwin":
        return OSX_DISTRIBUTION_NAME
    elif system == "windows":
        return WIN32_DISTRIBUTION_NAME

    return 'Unknown'


def main():
    parsed_args = parse_args()

    if parsed_args.dependencies:
        run_dependencies(parsed_args)

    elif parsed_args.init:
        run_init(parsed_args)

    elif parsed_args.clean:
        run_clean()

    elif parsed_args.install:
        run_install(parsed_args)

    elif parsed_args.uninstall:
        run_uninstall(parsed_args)

    elif parsed_args.run:
        if (parsed_args.distribution == 'guix'
                and 'GUIX_ENVIRONMENT' not in os.environ):
            # Relaunch this script, this time in a pure Guix environment.
            guix_args = ['shell', '--pure',
                         # to allow pulseaudio to connect to an existing server
                         "-E", "XAUTHORITY", "-E", "XDG_RUNTIME_DIR",
                         f'--manifest={GUIX_MANIFEST}', '--']
            args = sys.argv + ['--distribution=guix']
            print('Running in a guix shell spawned with: guix {}'
                  .format(str.join(' ', guix_args + args)))
            os.execlp('guix', 'guix', *(guix_args + args))
        else:
            run_run(parsed_args)

    elif parsed_args.stop:
        run_stop(parsed_args)


if __name__ == "__main__":
    main()