#!/usr/bin/python3

# SPDX-License-Identifier: GPL-3.0-or-later
#
# Copyright 2019 Mathieu Bridon
# Copyright 2021 Bastien Nocera

"""check-abi"""

import argparse
import contextlib
import glob
import os
import shutil
import subprocess
import sys

VERSION = '0.1'

def format_title(title):
    """Put title in a box"""
    box = {
        'tl': '╔', 'tr': '╗', 'bl': '╚', 'br': '╝', 'h': '═', 'v': '║',
    }
    hline = box['h'] * (len(title) + 2)

    return '\n'.join([
        f"{box['tl']}{hline}{box['tr']}",
        f"{box['v']} {title} {box['v']}",
        f"{box['bl']}{hline}{box['br']}",
    ])


def rm_rf(path):
    """rm -rf"""
    try:
        shutil.rmtree(path)
    except FileNotFoundError:
        pass


def sanitize_path(name):
    """Replace slashes with dashes"""
    return name.replace('/', '-')


def get_current_revision():
    """gets the current git revision"""
    revision = subprocess.check_output(['git', 'rev-parse', '--abbrev-ref', 'HEAD'],
                                       encoding='utf-8').strip()

    if revision == 'HEAD':
        # This is a detached HEAD, get the commit hash
        revision = subprocess.check_output(['git', 'rev-parse', 'HEAD']).strip().decode('utf-8')

    return revision


def get_meson_version():
    """Retrieves the version of Meson as a tuple"""
    res = subprocess.check_output(['meson', '--version']).strip().decode('utf-8')
    return [int(x) for x in res.split('.')]


def build_meson(build_dir, parameters, env):
    """Builds using Meson"""
    _setup_args = ['meson', 'setup', build_dir, '--prefix=/usr', '--libdir=lib']
    meson_version = get_meson_version()
    if meson_version[0] > 0 or (meson_version[0] == 0 and meson_version[1] >= 54):
        _compile_args = ['meson', 'compile', '-v', '-C', build_dir]
    else:
        _compile_args = ['ninja', '-v', '-C', build_dir]
    if meson_version[0] > 0 or (meson_version[0] == 0 and meson_version[1] >= 47):
        _install_args = ['meson', 'install', '-C', build_dir]
    else:
        _install_args = ['ninja', '-v', '-C', build_dir, 'install']
    if parameters is not None:
        _setup_args += parameters.split(' ')
    subprocess.check_call(_setup_args)
    subprocess.check_call(_compile_args)
    subprocess.check_call(_install_args, env=env)


def get_cmake_version():
    """Retrieves the version of CMake as a tuple"""
    res = subprocess.check_output(['cmake', '--version']).strip().decode('utf-8')
    lines = res.split('\n')
    ver = lines[0].replace('cmake version ', '')
    return [int(x) for x in ver.split('.')]


def build_cmake(build_dir, parameters, env):
    """Builds using CMake"""
    _setup_args = [
        "cmake",
        "-GNinja",
        "-B",
        build_dir,
        "-S",
        ".",
        "-DCMAKE_INSTALL_PREFIX=/usr",
        "-DCMAKE_INSTALL_LIBDIR=lib",
    ]
    _compile_args = ['cmake', '--build', build_dir]
    _install_args = ['cmake', '--install', build_dir]
    if parameters is not None:
        _setup_args += parameters.split(' ')
    subprocess.check_call(_setup_args)
    subprocess.check_call(_compile_args)
    subprocess.check_call(_install_args, env=env)


def build_autotools(revision, src_dir_copy, build_dir, parameters, env):
    """Builds using Autotools"""
    src_dir = os.getcwd()

    # Setup src dir copy
    os.mkdir(src_dir_copy)
    os.chdir(src_dir_copy)
    subprocess.check_call(['git', 'clone', src_dir, '.'])
    subprocess.check_call(['git', 'checkout', '-q', revision])
    subprocess.check_call(['sed',
                           '-i',
                           's/AM_GNU_GETTEXT_VERSION.*/AM_GNU_GETTEXT_VERSION([0.21])/',
                           'configure.ac'])
    os.chdir('..')

    os.mkdir(build_dir)
    os.chdir(build_dir)
    autogen_path = src_dir_copy + '/autogen.sh'
    _args = [autogen_path, '--prefix=/usr', '--libdir=/usr/lib']
    if parameters is not None:
        _args += parameters.split(' ')
    subprocess.check_call(_args)
    subprocess.check_call(['make'])
    subprocess.check_call(['make','install'], env=env)
    os.chdir(src_dir)


@contextlib.contextmanager
def checkout_git_revision(revision):
    """Checkout a git revision before reverting to the current one"""
    current_revision = get_current_revision()
    subprocess.check_call(['git', 'checkout', '-q', revision])

    try:
        yield
    finally:
        subprocess.check_call(['git', 'checkout', '-q', current_revision])


def build_install(revision, build_system, parameters):
    """Build git revision with the passed build system"""
    build_dir = '_check_abi_build'
    dest_dir = os.path.abspath(sanitize_path(revision))
    src_dir_copy = os.getcwd() + '/_src_copy'
    if not args.quiet:
        print(format_title(f'# Building and installing {revision} in {dest_dir}'),
              end='\n\n', flush=True)

    with checkout_git_revision(revision):
        rm_rf(build_dir)
        rm_rf(revision)
        rm_rf(src_dir_copy)

        env = os.environ.copy()
        env['DESTDIR'] = dest_dir
        env['V'] = '1'

        if not build_system or build_system == 'autodetect':
            if os.path.exists('meson.build'):
                build_system = 'meson'
            elif os.path.exists('CMakeLists.txt'):
                build_system = 'cmake'
            elif os.path.exists('configure.ac') and os.path.exists('autogen.sh'):
                build_system = 'autotools'
            else:
                print('Could not detect build system', file=sys.stderr)
                assert()

        if build_system == 'meson':
            build_meson(build_dir, parameters, env)
        elif build_system == 'cmake':
            build_cmake(build_dir, parameters, env)
        elif build_system == 'autotools':
            build_autotools(revision, src_dir_copy, build_dir, parameters, env)
        else:
            print(f'Unsupported build system "f{build_system}"', file=sys.stderr)
            assert()

    return dest_dir


def compare(_old_tree, _new_tree):
    """Compare the ABIs between 2 build trees"""
    if not args.quiet:
        print(format_title('# Comparing the two ABIs'), end='\n\n', flush=True)

    if not args.library_name:
        paths = glob.glob(os.path.join(_old_tree, 'usr', 'lib') + '/*.so')
        libs_name = []
        for path in paths:
            libs_name.append(os.path.basename(path))
    else:
        libs_name = [ args.library_name ]

    for lib_name in libs_name:

        old_headers = os.path.join(_old_tree, 'usr', 'include')
        old_lib = os.path.join(_old_tree, 'usr', 'lib', lib_name)

        new_headers = os.path.join(_new_tree, 'usr', 'include')
        new_lib = os.path.join(_new_tree, 'usr', 'lib', lib_name)

        cmd = [
                'abidiff', '--headers-dir1', old_headers, '--headers-dir2', new_headers,
                '--drop-private-types', '--fail-no-debug-info', '--no-added-syms'
                ]
        if args.suppr:
            cmd.append ('--suppr')
            cmd.append (args.suppr)

        cmd.append (old_lib)
        cmd.append (new_lib)

        subprocess.check_call(cmd)


if __name__ == '__main__':
    parser = argparse.ArgumentParser()

    parser.add_argument('old', help='the previous git revision, considered the reference')
    parser.add_argument('new', help='the new git revision, to compare to the reference')
    parser.add_argument('-q', '--quiet', action='store_true', help='increase output verbosity')
    parser.add_argument('-s', '--suppr', help='Pass a suppression file to abidiff')
    parser.add_argument('--old-build-system',
            help='build system to use for the reference git revision (default: autodetect)',
            type=str, choices=['autodetect', 'meson',  'cmake', 'autotools'])
    parser.add_argument('--new-build-system',
            help='build system to use for the new git revision (default: autodetect)',
            type=str, choices=['autodetect', 'meson', 'cmake', 'autotools'])
    parser.add_argument('--old-parameters',
            help='additional arguments to pass to the reference git revisionʼs build system',
            type=str)
    parser.add_argument('--new-parameters',
            help='additional arguments to pass to the new git revisionʼs build system',
            type=str)
    parser.add_argument('--parameters',
            help="additional arguments to pass to both the old and the"
                 " new git revisionʼs build systems",
            type=str)
    parser.add_argument('-l', '--library-name',
            help='the name of the shared library to compare (default: \'*.so\')',
            type=str)

    args = parser.parse_args()

    if args.parameters:
        if args.old_parameters or args.new_parameters:
            if not args.quiet:
                print("Can't pass --parameters with either --old-parameters"
                      " or --new-parameters")
            sys.exit(1)
        args.old_parameters = args.parameters
        args.new_parameters = args.parameters

    if args.old == args.new and args.old_parameters == args.new_parameters:
        if not args.quiet:
            print("Let's not waste time comparing something to itself")
        sys.exit(0)

    old_tree = build_install(args.old, args.old_build_system, args.old_parameters)
    new_tree = build_install(args.new, args.new_build_system, args.new_parameters)

    try:
        compare(old_tree, new_tree)

    except Exception: # pylint: disable=broad-except
        sys.exit(1)

    if not args.quiet:
        print(f'Hurray! {args.old} and {args.new} are ABI-compatible!')