#!/usr/libexec/platform-python
## -*- coding: utf-8 -*- vim:shiftwidth=4:expandtab:
##
## RPM: elf-requires for OSSTech vendor specific libraries
## Copyright (c) 2022 SATOH Fumiyasu @ OSSTech Corp., Japan
##               <https://www.OSSTech.co.jp/>
##
## License: GNU General Public License version 3 or later
##

import logging
import os
import sys
import re
import subprocess
import itertools

logger = logging.getLogger(__name__)

vendor_deps = os.environ.get('VENDOR_RPM_VENDOR_DEPENDENCIES', False)

vendor_id = os.environ.get('VENDOR_RPM_VENDOR_ID', 'osstech')
vendor_prefix_slash = os.environ.get('VENDOR_RPM_VENDOR_PREFIX', '/opt/osstech')
vendor_prefix_slash += '/'

rpm_build_root = os.environ.get('RPM_BUILD_ROOT')

nargs_max = 100
objdump = os.environ.get('VENDOR_RPM_OBJDUMP', 'objdump')
objdump_cmd = [objdump, '-p']
objdump_re_format = re.compile(r'^(?P<fname>/[^:]+): +file format (?P<format>.*)$')
objdump_re_needed_libname = re.compile(r'^  NEEDED +(?P<libname>(lib|ld-)\S+\.so(\.[^:]*)?)$')
objdump_re_needed_rtld_typename = re.compile(r'^  (?P<typename>GNU_HASH) +0x[0-9a-f]+$')
objdump_re_runpath = re.compile(r'^  (?P<name>RUNPATH|RPATH) +(?P<runpath>/.*)$')
objdump_re_ver_refs_section = re.compile(r'^Version References:')
objdump_re_required_from = re.compile(r'^  required from (?P<libname>(lib|ld-)\S+\.so(\.[^:]*)?):$')
objdump_re_required_libver = re.compile(r'^    0x[0-9a-f]+ 0x[0-9a-f]+ [0-9]+ (?P<libver>\S+)$')


def main(argv):
    logger.debug('Arguments: %s', argv)
    logger.debug('objdump: %s', objdump_cmd)

    libdir = argv[0]

    proc_env = os.environ.copy()
    proc_env['LC_ALL'] = 'C'

    requires_libs_by_bits = {
        '': {},
        '64bit': {},
    }
    requires_vendor_libs = set()
    requires_rtld_typenames = set()

    while True:
        fnames = [x.rstrip() for x in itertools.islice(sys.stdin, nargs_max)]
        if not fnames:
            break

        logger.debug('Target filenames: %s', fnames)
        proc = subprocess.Popen(
            objdump_cmd + fnames,
            stdin=subprocess.DEVNULL,
            stdout=subprocess.PIPE,
            env=proc_env,
            encoding='UTF-8',
            universal_newlines=True,
        )

        for line in proc.stdout:
            line = line.rstrip()
            logger.debug('Line: %s', line)

            ## File format (`/path/to/filename: file format elf64-...`)
            m = objdump_re_format.search(line)
            if m:
                logger.info('Target filename: %s', m['fname'])
                ctx = {
                    'name': m['fname'],
                    'bits': ('64bit' if m['format'].endswith('64') else ''),
                    'libnames': [],
                    'runpaths': [libdir],
                    'vendor_dist_name_by_libname': {},
                }
                continue

            ## Required libraries
            ## (`NEEDED libfoo.so.1` in `Dynamic Section:`)
            m = objdump_re_needed_libname.search(line)
            if m:
                logger.info('%s: NEEDED: %s', ctx['name'], m['libname'])
                ctx['libnames'].append(m['libname'])
                continue

            ## Classify vendor specific libraries when enter to `Version References:` section
            m = objdump_re_ver_refs_section.search(line)
            if m:
                for libname in ctx['libnames']:
                    ctx['vendor_dist_name_by_libname'][libname] = False
                    for runpath in ctx['runpaths']:
                        if not runpath.startswith(vendor_prefix_slash):
                            next
                        libpath = f'{runpath}/{libname}'
                        if rpm_build_root and os.path.isfile(f'{rpm_build_root}{libpath}') or \
                           os.path.isfile(libpath):
                            vendor_dist_name = os.path.basename(runpath)
                            ctx['vendor_dist_name_by_libname'][libname] = vendor_dist_name
                            requires_vendor_libs.add(libpath)
                            break
                    if libname not in requires_libs_by_bits[ctx['bits']]:
                        requires_libs_by_bits[ctx['bits']][libname] = {
                            'vers': set(),
                        }
                continue

            ## Required library version from
            ## (`required from libfoo.so.1:` in `Version References:` section)
            m = objdump_re_required_from.search(line)
            if m:
                libname = m['libname']
                ctx['libname'] = libname
                continue

            ## Required library version
            ## (`0xXXXXXXXX 0xXX 00 FOO_1.0` in `Version References:` section)
            m = objdump_re_required_libver.search(line)
            if m:
                logger.info('%s: LIBVER: %s %s', ctx['name'], ctx['libname'], m['libver'])
                requires_libs_by_bits[ctx['bits']][ctx['libname']]['vers'].add(m['libver'])
                continue

            ## Required runtime linker type
            ## (`GNU_HASH` in `Dynamic Section:` section)
            m = objdump_re_needed_rtld_typename.search(line)
            if m:
                logger.info('%s: RTLD: %s', ctx['name'], m['typename'])
                requires_rtld_typenames.add(m['typename'])
                continue

            ## Library search paths
            ## (`RUNPATH /opt/osstech/lib64/baz:/opt/osstech/lib64/qux` in `Dynamic Section:` section)
            m = objdump_re_runpath.search(line)
            if m:
                logger.info('%s: %s: %s', ctx['name'], m['name'], m['runpath'])
                ctx['runpaths'].extend(m['runpath'].split(':'))
                continue

        proc.wait()
        if proc.returncode != 0:
            logger.info('%s exited with error: %d', objdump, proc.returncode)
            return(proc.returncode)

    for bits, requires in requires_libs_by_bits.items():
        for libname, lib in requires.items():
            vendor_dist_name = ctx['vendor_dist_name_by_libname'].get(libname)
            if vendor_dist_name:
                if not vendor_deps:
                    ## Discard all dependencies for files bundled in vendor packages
                    continue
                libver_prefix = f'{vendor_id}-{vendor_dist_name}/'
            else:
                libver_prefix = ''
            print(f'{libname}({libver_prefix})({bits})')
            for libver in lib['vers']:
                print(f'{libname}({libver_prefix}{libver})({bits})')

    for lib in requires_vendor_libs:
        print(lib)

    for typename in requires_rtld_typenames:
        print(f'rtld({typename})')

    return 0


if __name__ == '__main__':
    log_level = getattr(
        logging,
        os.environ.get('VENDOR_RPM_ELF_REQUIRES_LOG_LEVEL', '').upper(),
        logging.WARN
    )
    logging.basicConfig(
        level=log_level,
        format=f'{sys.argv[0]}: %(levelname)s: %(message)s',
        handlers=[logging.StreamHandler()],
    )

    if len(sys.argv) < 3:
        print("Usage: %s LIBDIR RPM_ELF_REQUIRES_COMMAND [...]" % (sys.argv[0]), file=sys.stderr)
        sys.exit(1)

    sys.exit(main(sys.argv[1:]))
