diff --git a/src/Makefile b/src/Makefile new file mode 100644 index 0000000000000000000000000000000000000000..7b87d4cfcd5b701e797abf8683ccfb0af1880bbc --- /dev/null +++ b/src/Makefile @@ -0,0 +1,31 @@ +prefix ?= /usr +modules = abicheck +cmd = abi-info-check-online + +modules_dir = $(DESTDIR)$(prefix)/share/$(modules) +bin_dir = $(DESTDIR)$(prefix)/bin +man1_dir = $(DESTDIR)$(prefix)/share/man/man1 + +.PHONY: install uninstall + +install: + mkdir -p $(modules_dir) + cp -r conf $(modules_dir)/ + + python3 ./$(cmd).py -v + mkdir -p $(man1_dir) + mkdir -p $(man1_dir) + install -m 644 ${cmd}.1 $(man1_dir)/ + + $(HOME)/.local/bin/pyinstaller -F $(cmd).py + install -m 755 dist/$(cmd) $(bin_dir)/ + +uninstall: + rm -f $(bin_dir)/$(cmd) + rm -f $(man1_dir)/$(cmd).1* + rm -rf $(modules_dir) + +clean: + rm -rf build/ dist/ __pycache__/ +build: + $(HOME)/.local/bin/pyinstaller -F $(cmd).py diff --git a/src/__init__.py b/src/__init__.py index 5becc17c04a9e3ad1c2a15f53252b7bb5a7517e7..b2a95f90233edf6694650eba0827eb523ee23be6 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -1 +1 @@ -__version__ = "1.0.0" +__version__ = "1.2" diff --git a/src/abi-info-check.py b/src/abi-info-check.py deleted file mode 100644 index 46ef386c8be7e989604047996d23ab142446df30..0000000000000000000000000000000000000000 --- a/src/abi-info-check.py +++ /dev/null @@ -1,129 +0,0 @@ -#!/usr/bin/python3 - -# -*- coding: utf-8 -*- - -import logging -import os -import re -import subprocess -import sys -import tarfile - -import distro - -from utils import * -from toolopts import * - -TOOL_VERSION = "1.0" -ABI_CC = "abi-compliance-checker" -ABI_DUMPER = "abi-dumper" - -CMD_NAME = os.path.basename(__file__) -ERROR_CODE = {"Ok": 0, "Error": 1, "Empty": 10, "NoDebug": 11, "NoABI": 12} - -ARGS = {} - -def detect_os(): - if distro.id != 'uos': - logging.error('please run in UOS') - exit() - -def extract_tarball(tarball, export_dir): - print(f'Decompressing file {tarball} ...') - with tarfile.open(tarball, 'r:gz') as tar: - tar.extractall(path=export_dir) - print(f'The file {tarball} has been decompressed.') - - -def get_rpmname_without_libs(x): - # 正则获取rpmname - return re.sub('-libs$', '', x).strip() -def s_exit(code): - sys.exit(ERROR_CODE[code]) - -def print_err(msg): - sys.stderr.write(msg+"\n") - -def exit_status(code, msg): - if code != "Ok": - print_err("ERROR: "+msg) - else: - print(msg) - - s_exit(code) - -def main(): - global ARGS - ARGS = parse_args() - - # 检查参数 - if not ARGS.tar: - exit_status('Error', 'tarball file are not specified (-tar option)') - - tarball = ARGS.tar - if not os.path.isfile(tarball): - exit_status('Error', f'''file "{tarball}" does not exist.''') - if not os.access(tarball, os.R_OK): - exit_status('Error', f'''file "{tarball}" can't be read.''') - if not tarfile.is_tarfile(tarball): - exit_status( - 'Error', f'''file "{tarball}" isn't a standard tarball file.''') - - if ARGS.export_dir: - export_dir = ARGS.export_dir - else: - export_dir = "abi-info-export" - - if not os.path.exists(export_dir): - os.makedirs(export_dir) - - # 检查 abi-compliance-checker 是否安装 - global ABI_CC, ABI_DUMPER - if not check_cmd(ABI_CC): - exit_status('Error', 'ABI Compliance Checker is not installed') - - # 检查 abi-dumper 是否安装 - if not check_cmd(ABI_DUMPER): - exit_status('Error', 'ABI Dumper is not installed') - - # 判断系统 - # detect_os() - - # 解压 tarball - extract_tarball(tarball, export_dir) - - # 获取OLD-name 值 - abi_name = get_abi_name(export_dir) - - # 解析 dev 包 和 lib 包 - dev_pkgs, libs_pkgs, debuginfo_pkgs = find_devel_lib_package(export_dir) - - if not ARGS.debuginfo: - - # 判断系统中是否安装 dev 包 和 lib 包 - try_install_packages(dev_pkgs, libs_pkgs) - # 生成 abi-compliance-checker 用到的 xml 文件,并生成 dump 文件 - dump_file = gen_xml_and_dump( - abi_name, export_dir, dev_pkgs, libs_pkgs) - else: - # 判断系统中是否安装 debuginfo_pkgs 包 - try_install_packages(debuginfo_pkgs) - # 通过 debuginfo 信息生成 dump 文件 - dump_file = gen_dump_with_debuginfo(abi_name, export_dir, - debuginfo_pkgs) - - # 读取 dump 文件,分析 symbol - #syms_list = compare_syms(export_dir, dump_file) - # 比较前后两个dump 文件 - html = check_dump_syms(abi_name, export_dir) - - if os.path.isfile(html): - print(f'The check result is {html}') - else: - exit_status("Error", "Checking error") - # 输出结果 - # output_result(export_dir) - - -if __name__ == "__main__": - main() diff --git a/src/abi-info-collect.py b/src/abi-info-collect.py deleted file mode 100644 index 15a73157d01a555c0be03133378177887c27c170..0000000000000000000000000000000000000000 --- a/src/abi-info-collect.py +++ /dev/null @@ -1,1550 +0,0 @@ -#!/usr/bin/python3 - -# -*- coding: utf-8 -*- - -import argparse -import logging -import os -import re -import shlex -import shutil -import subprocess -import sys -import tarfile - -TOOL_VERSION = "1.0" - -_UNIXCONFDIR = os.environ.get('UNIXCONFDIR', '/etc') -_OS_RELEASE_BASENAME = 'os-release' - -#: Translation table for normalizing the "ID" attribute defined in os-release -#: files, for use by the :func:`distro.id` method. -#: -#: * Key: Value as defined in the os-release file, translated to lower case, -#: with blanks translated to underscores. -#: -#: * Value: Normalized value. -NORMALIZED_OS_ID = { - 'ol': 'oracle', # Oracle Linux -} - -#: Translation table for normalizing the "Distributor ID" attribute returned by -#: the lsb_release command, for use by the :func:`distro.id` method. -#: -#: * Key: Value as returned by the lsb_release command, translated to lower -#: case, with blanks translated to underscores. -#: -#: * Value: Normalized value. -NORMALIZED_LSB_ID = { - 'enterpriseenterpriseas': 'oracle', # Oracle Enterprise Linux 4 - 'enterpriseenterpriseserver': 'oracle', # Oracle Linux 5 - 'redhatenterpriseworkstation': 'rhel', # RHEL 6, 7 Workstation - 'redhatenterpriseserver': 'rhel', # RHEL 6, 7 Server - 'redhatenterprisecomputenode': 'rhel', # RHEL 6 ComputeNode -} - -#: Translation table for normalizing the distro ID derived from the file name -#: of distro release files, for use by the :func:`distro.id` method. -#: -#: * Key: Value as derived from the file name of a distro release file, -#: translated to lower case, with blanks translated to underscores. -#: -#: * Value: Normalized value. -NORMALIZED_DISTRO_ID = { - 'redhat': 'rhel', # RHEL 6.x, 7.x -} - -# Pattern for content of distro release file (reversed) -_DISTRO_RELEASE_CONTENT_REVERSED_PATTERN = re.compile( - r'(?:[^)]*\)(.*)\()? *(?:STL )?([\d.+\-a-z]*\d) *(?:esaeler *)?(.+)') - -# Pattern for base file name of distro release file -_DISTRO_RELEASE_BASENAME_PATTERN = re.compile( - r'(\w+)[-_](release|version)$') - -# Base file names to be ignored when searching for distro release file -_DISTRO_RELEASE_IGNORE_BASENAMES = ( - 'debian_version', - 'lsb-release', - 'oem-release', - _OS_RELEASE_BASENAME, - 'system-release', - 'plesk-release', -) - - -def linux_distribution(full_distribution_name=True): - """ - Return information about the current OS distribution as a tuple - ``(id_name, version, codename)`` with items as follows: - - * ``id_name``: If *full_distribution_name* is false, the result of - :func:`distro.id`. Otherwise, the result of :func:`distro.name`. - - * ``version``: The result of :func:`distro.version`. - - * ``codename``: The result of :func:`distro.codename`. - - The interface of this function is compatible with the original - :py:func:`platform.linux_distribution` function, supporting a subset of - its parameters. - - The data it returns may not exactly be the same, because it uses more data - sources than the original function, and that may lead to different data if - the OS distribution is not consistent across multiple data sources it - provides (there are indeed such distributions ...). - - Another reason for differences is the fact that the :func:`distro.id` - method normalizes the distro ID string to a reliable machine-readable value - for a number of popular OS distributions. - """ - return _distro.linux_distribution(full_distribution_name) - - -def id(): - """ - Return the distro ID of the current distribution, as a - machine-readable string. - - For a number of OS distributions, the returned distro ID value is - *reliable*, in the sense that it is documented and that it does not change - across releases of the distribution. - - This package maintains the following reliable distro ID values: - - ============== ========================================= - Distro ID Distribution - ============== ========================================= - "ubuntu" Ubuntu - "debian" Debian - "rhel" RedHat Enterprise Linux - "centos" CentOS - "fedora" Fedora - "sles" SUSE Linux Enterprise Server - "opensuse" openSUSE - "amazon" Amazon Linux - "arch" Arch Linux - "cloudlinux" CloudLinux OS - "exherbo" Exherbo Linux - "gentoo" GenToo Linux - "ibm_powerkvm" IBM PowerKVM - "kvmibm" KVM for IBM z Systems - "linuxmint" Linux Mint - "mageia" Mageia - "mandriva" Mandriva Linux - "parallels" Parallels - "pidora" Pidora - "raspbian" Raspbian - "oracle" Oracle Linux (and Oracle Enterprise Linux) - "scientific" Scientific Linux - "slackware" Slackware - "xenserver" XenServer - "openbsd" OpenBSD - "netbsd" NetBSD - "freebsd" FreeBSD - "midnightbsd" MidnightBSD - ============== ========================================= - - If you have a need to get distros for reliable IDs added into this set, - or if you find that the :func:`distro.id` function returns a different - distro ID for one of the listed distros, please create an issue in the - `distro issue tracker`_. - - **Lookup hierarchy and transformations:** - - First, the ID is obtained from the following sources, in the specified - order. The first available and non-empty value is used: - - * the value of the "ID" attribute of the os-release file, - - * the value of the "Distributor ID" attribute returned by the lsb_release - command, - - * the first part of the file name of the distro release file, - - The so determined ID value then passes the following transformations, - before it is returned by this method: - - * it is translated to lower case, - - * blanks (which should not be there anyway) are translated to underscores, - - * a normalization of the ID is performed, based upon - `normalization tables`_. The purpose of this normalization is to ensure - that the ID is as reliable as possible, even across incompatible changes - in the OS distributions. A common reason for an incompatible change is - the addition of an os-release file, or the addition of the lsb_release - command, with ID values that differ from what was previously determined - from the distro release file name. - """ - return _distro.id() - - -def name(pretty=False): - """ - Return the name of the current OS distribution, as a human-readable - string. - - If *pretty* is false, the name is returned without version or codename. - (e.g. "CentOS Linux") - - If *pretty* is true, the version and codename are appended. - (e.g. "CentOS Linux 7.1.1503 (Core)") - - **Lookup hierarchy:** - - The name is obtained from the following sources, in the specified order. - The first available and non-empty value is used: - - * If *pretty* is false: - - - the value of the "NAME" attribute of the os-release file, - - - the value of the "Distributor ID" attribute returned by the lsb_release - command, - - - the value of the "" field of the distro release file. - - * If *pretty* is true: - - - the value of the "PRETTY_NAME" attribute of the os-release file, - - - the value of the "Description" attribute returned by the lsb_release - command, - - - the value of the "" field of the distro release file, appended - with the value of the pretty version ("" and "" - fields) of the distro release file, if available. - """ - return _distro.name(pretty) - - -def version(pretty=False, best=False): - """ - Return the version of the current OS distribution, as a human-readable - string. - - If *pretty* is false, the version is returned without codename (e.g. - "7.0"). - - If *pretty* is true, the codename in parenthesis is appended, if the - codename is non-empty (e.g. "7.0 (Maipo)"). - - Some distributions provide version numbers with different precisions in - the different sources of distribution information. Examining the different - sources in a fixed priority order does not always yield the most precise - version (e.g. for Debian 8.2, or CentOS 7.1). - - The *best* parameter can be used to control the approach for the returned - version: - - If *best* is false, the first non-empty version number in priority order of - the examined sources is returned. - - If *best* is true, the most precise version number out of all examined - sources is returned. - - **Lookup hierarchy:** - - In all cases, the version number is obtained from the following sources. - If *best* is false, this order represents the priority order: - - * the value of the "VERSION_ID" attribute of the os-release file, - * the value of the "Release" attribute returned by the lsb_release - command, - * the version number parsed from the "" field of the first line - of the distro release file, - * the version number parsed from the "PRETTY_NAME" attribute of the - os-release file, if it follows the format of the distro release files. - * the version number parsed from the "Description" attribute returned by - the lsb_release command, if it follows the format of the distro release - files. - """ - return _distro.version(pretty, best) - - -def version_parts(best=False): - """ - Return the version of the current OS distribution as a tuple - ``(major, minor, build_number)`` with items as follows: - - * ``major``: The result of :func:`distro.major_version`. - - * ``minor``: The result of :func:`distro.minor_version`. - - * ``build_number``: The result of :func:`distro.build_number`. - - For a description of the *best* parameter, see the :func:`distro.version` - method. - """ - return _distro.version_parts(best) - - -def major_version(best=False): - """ - Return the major version of the current OS distribution, as a string, - if provided. - Otherwise, the empty string is returned. The major version is the first - part of the dot-separated version string. - - For a description of the *best* parameter, see the :func:`distro.version` - method. - """ - return _distro.major_version(best) - - -def minor_version(best=False): - """ - Return the minor version of the current OS distribution, as a string, - if provided. - Otherwise, the empty string is returned. The minor version is the second - part of the dot-separated version string. - - For a description of the *best* parameter, see the :func:`distro.version` - method. - """ - return _distro.minor_version(best) - - -def build_number(best=False): - """ - Return the build number of the current OS distribution, as a string, - if provided. - Otherwise, the empty string is returned. The build number is the third part - of the dot-separated version string. - - For a description of the *best* parameter, see the :func:`distro.version` - method. - """ - return _distro.build_number(best) - - -def like(): - """ - Return a space-separated list of distro IDs of distributions that are - closely related to the current OS distribution in regards to packaging - and programming interfaces, for example distributions the current - distribution is a derivative from. - - **Lookup hierarchy:** - - This information item is only provided by the os-release file. - For details, see the description of the "ID_LIKE" attribute in the - `os-release man page - `_. - """ - return _distro.like() - - -def codename(): - """ - Return the codename for the release of the current OS distribution, - as a string. - - If the distribution does not have a codename, an empty string is returned. - - Note that the returned codename is not always really a codename. For - example, openSUSE returns "x86_64". This function does not handle such - cases in any special way and just returns the string it finds, if any. - - **Lookup hierarchy:** - - * the codename within the "VERSION" attribute of the os-release file, if - provided, - - * the value of the "Codename" attribute returned by the lsb_release - command, - - * the value of the "" field of the distro release file. - """ - return _distro.codename() - - -def info(pretty=False, best=False): - """ - Return certain machine-readable information items about the current OS - distribution in a dictionary, as shown in the following example: - - .. sourcecode:: python - - { - 'id': 'rhel', - 'version': '7.0', - 'version_parts': { - 'major': '7', - 'minor': '0', - 'build_number': '' - }, - 'like': 'fedora', - 'codename': 'Maipo' - } - - The dictionary structure and keys are always the same, regardless of which - information items are available in the underlying data sources. The values - for the various keys are as follows: - - * ``id``: The result of :func:`distro.id`. - - * ``version``: The result of :func:`distro.version`. - - * ``version_parts -> major``: The result of :func:`distro.major_version`. - - * ``version_parts -> minor``: The result of :func:`distro.minor_version`. - - * ``version_parts -> build_number``: The result of - :func:`distro.build_number`. - - * ``like``: The result of :func:`distro.like`. - - * ``codename``: The result of :func:`distro.codename`. - - For a description of the *pretty* and *best* parameters, see the - :func:`distro.version` method. - """ - return _distro.info(pretty, best) - - -def os_release_info(): - """ - Return a dictionary containing key-value pairs for the information items - from the os-release file data source of the current OS distribution. - - See `os-release file`_ for details about these information items. - """ - return _distro.os_release_info() - - -def lsb_release_info(): - """ - Return a dictionary containing key-value pairs for the information items - from the lsb_release command data source of the current OS distribution. - - See `lsb_release command output`_ for details about these information - items. - """ - return _distro.lsb_release_info() - - -def distro_release_info(): - """ - Return a dictionary containing key-value pairs for the information items - from the distro release file data source of the current OS distribution. - - See `distro release file`_ for details about these information items. - """ - return _distro.distro_release_info() - - -def uname_info(): - """ - Return a dictionary containing key-value pairs for the information items - from the distro release file data source of the current OS distribution. - """ - return _distro.uname_info() - - -def os_release_attr(attribute): - """ - Return a single named information item from the os-release file data source - of the current OS distribution. - - Parameters: - - * ``attribute`` (string): Key of the information item. - - Returns: - - * (string): Value of the information item, if the item exists. - The empty string, if the item does not exist. - - See `os-release file`_ for details about these information items. - """ - return _distro.os_release_attr(attribute) - - -def lsb_release_attr(attribute): - """ - Return a single named information item from the lsb_release command output - data source of the current OS distribution. - - Parameters: - - * ``attribute`` (string): Key of the information item. - - Returns: - - * (string): Value of the information item, if the item exists. - The empty string, if the item does not exist. - - See `lsb_release command output`_ for details about these information - items. - """ - return _distro.lsb_release_attr(attribute) - - -def distro_release_attr(attribute): - """ - Return a single named information item from the distro release file - data source of the current OS distribution. - - Parameters: - - * ``attribute`` (string): Key of the information item. - - Returns: - - * (string): Value of the information item, if the item exists. - The empty string, if the item does not exist. - - See `distro release file`_ for details about these information items. - """ - return _distro.distro_release_attr(attribute) - - -def uname_attr(attribute): - """ - Return a single named information item from the distro release file - data source of the current OS distribution. - - Parameters: - - * ``attribute`` (string): Key of the information item. - - Returns: - - * (string): Value of the information item, if the item exists. - The empty string, if the item does not exist. - """ - return _distro.uname_attr(attribute) - - -class cached_property(object): - """A version of @property which caches the value. On access, it calls the - underlying function and sets the value in `__dict__` so future accesses - will not re-call the property. - """ - - def __init__(self, f): - self._fname = f.__name__ - self._f = f - - def __get__(self, obj, owner): - assert obj is not None, 'call {} on an instance'.format(self._fname) - ret = obj.__dict__[self._fname] = self._f(obj) - return ret - - -class LinuxDistribution(object): - """ - Provides information about a OS distribution. - - This package creates a private module-global instance of this class with - default initialization arguments, that is used by the - `consolidated accessor functions`_ and `single source accessor functions`_. - By using default initialization arguments, that module-global instance - returns data about the current OS distribution (i.e. the distro this - package runs on). - - Normally, it is not necessary to create additional instances of this class. - However, in situations where control is needed over the exact data sources - that are used, instances of this class can be created with a specific - distro release file, or a specific os-release file, or without invoking the - lsb_release command. - """ - - def __init__(self, - include_lsb=True, - os_release_file='', - distro_release_file='', - include_uname=True): - """ - The initialization method of this class gathers information from the - available data sources, and stores that in private instance attributes. - Subsequent access to the information items uses these private instance - attributes, so that the data sources are read only once. - - Parameters: - - * ``include_lsb`` (bool): Controls whether the - `lsb_release command output`_ is included as a data source. - - If the lsb_release command is not available in the program execution - path, the data source for the lsb_release command will be empty. - - * ``os_release_file`` (string): The path name of the - `os-release file`_ that is to be used as a data source. - - An empty string (the default) will cause the default path name to - be used (see `os-release file`_ for details). - - If the specified or defaulted os-release file does not exist, the - data source for the os-release file will be empty. - - * ``distro_release_file`` (string): The path name of the - `distro release file`_ that is to be used as a data source. - - An empty string (the default) will cause a default search algorithm - to be used (see `distro release file`_ for details). - - If the specified distro release file does not exist, or if no default - distro release file can be found, the data source for the distro - release file will be empty. - - * ``include_uname`` (bool): Controls whether uname command output is - included as a data source. If the uname command is not available in - the program execution path the data source for the uname command will - be empty. - - Public instance attributes: - - * ``os_release_file`` (string): The path name of the - `os-release file`_ that is actually used as a data source. The - empty string if no distro release file is used as a data source. - - * ``distro_release_file`` (string): The path name of the - `distro release file`_ that is actually used as a data source. The - empty string if no distro release file is used as a data source. - - * ``include_lsb`` (bool): The result of the ``include_lsb`` parameter. - This controls whether the lsb information will be loaded. - - * ``include_uname`` (bool): The result of the ``include_uname`` - parameter. This controls whether the uname information will - be loaded. - - Raises: - - * :py:exc:`IOError`: Some I/O issue with an os-release file or distro - release file. - - * :py:exc:`subprocess.CalledProcessError`: The lsb_release command had - some issue (other than not being available in the program execution - path). - - * :py:exc:`UnicodeError`: A data source has unexpected characters or - uses an unexpected encoding. - """ - self.os_release_file = os_release_file or \ - os.path.join(_UNIXCONFDIR, _OS_RELEASE_BASENAME) - self.distro_release_file = distro_release_file or '' # updated later - self.include_lsb = include_lsb - self.include_uname = include_uname - - def __repr__(self): - """Return repr of all info - """ - return \ - "LinuxDistribution(" \ - "os_release_file={self.os_release_file!r}, " \ - "distro_release_file={self.distro_release_file!r}, " \ - "include_lsb={self.include_lsb!r}, " \ - "include_uname={self.include_uname!r}, " \ - "_os_release_info={self._os_release_info!r}, " \ - "_lsb_release_info={self._lsb_release_info!r}, " \ - "_distro_release_info={self._distro_release_info!r}, " \ - "_uname_info={self._uname_info!r})".format( - self=self) - - def linux_distribution(self, full_distribution_name=True): - """ - Return information about the OS distribution that is compatible - with Python's :func:`platform.linux_distribution`, supporting a subset - of its parameters. - - For details, see :func:`distro.linux_distribution`. - """ - return ( - self.name() if full_distribution_name else self.id(), - self.version(), - self.codename() - ) - - def id(self): - """Return the distro ID of the OS distribution, as a string. - - For details, see :func:`distro.id`. - """ - def normalize(distro_id, table): - distro_id = distro_id.lower().replace(' ', '_') - return table.get(distro_id, distro_id) - - distro_id = self.os_release_attr('id') - if distro_id: - return normalize(distro_id, NORMALIZED_OS_ID) - - distro_id = self.lsb_release_attr('distributor_id') - if distro_id: - return normalize(distro_id, NORMALIZED_LSB_ID) - - distro_id = self.distro_release_attr('id') - if distro_id: - return normalize(distro_id, NORMALIZED_DISTRO_ID) - - distro_id = self.uname_attr('id') - if distro_id: - return normalize(distro_id, NORMALIZED_DISTRO_ID) - - return '' - - def name(self, pretty=False): - """ - Return the name of the OS distribution, as a string. - - For details, see :func:`distro.name`. - """ - name = self.os_release_attr('name') \ - or self.lsb_release_attr('distributor_id') \ - or self.distro_release_attr('name') \ - or self.uname_attr('name') - if pretty: - name = self.os_release_attr('pretty_name') \ - or self.lsb_release_attr('description') - if not name: - name = self.distro_release_attr('name') \ - or self.uname_attr('name') - version = self.version(pretty=True) - if version: - name = name + ' ' + version - return name or '' - - def version(self, pretty=False, best=False): - """ - Return the version of the OS distribution, as a string. - - For details, see :func:`distro.version`. - """ - versions = [ - self.os_release_attr('version_id'), - self.lsb_release_attr('release'), - self.distro_release_attr('version_id'), - self._parse_distro_release_content( - self.os_release_attr('pretty_name')).get('version_id', ''), - self._parse_distro_release_content( - self.lsb_release_attr('description')).get('version_id', ''), - self.uname_attr('release') - ] - version = '' - if best: - # This algorithm uses the last version in priority order that has - # the best precision. If the versions are not in conflict, that - # does not matter; otherwise, using the last one instead of the - # first one might be considered a surprise. - for v in versions: - if v.count(".") > version.count(".") or version == '': - version = v - else: - for v in versions: - if v != '': - version = v - break - if pretty and version and self.codename(): - version = '{0} ({1})'.format(version, self.codename()) - return version - - def version_parts(self, best=False): - """ - Return the version of the OS distribution, as a tuple of version - numbers. - - For details, see :func:`distro.version_parts`. - """ - version_str = self.version(best=best) - if version_str: - version_regex = re.compile(r'(\d+)\.?(\d+)?\.?(\d+)?') - matches = version_regex.match(version_str) - if matches: - major, minor, build_number = matches.groups() - return major, minor or '', build_number or '' - return '', '', '' - - def major_version(self, best=False): - """ - Return the major version number of the current distribution. - - For details, see :func:`distro.major_version`. - """ - return self.version_parts(best)[0] - - def minor_version(self, best=False): - """ - Return the minor version number of the current distribution. - - For details, see :func:`distro.minor_version`. - """ - return self.version_parts(best)[1] - - def build_number(self, best=False): - """ - Return the build number of the current distribution. - - For details, see :func:`distro.build_number`. - """ - return self.version_parts(best)[2] - - def like(self): - """ - Return the IDs of distributions that are like the OS distribution. - - For details, see :func:`distro.like`. - """ - return self.os_release_attr('id_like') or '' - - def codename(self): - """ - Return the codename of the OS distribution. - - For details, see :func:`distro.codename`. - """ - try: - # Handle os_release specially since distros might purposefully set - # this to empty string to have no codename - return self._os_release_info['codename'] - except KeyError: - return self.lsb_release_attr('codename') \ - or self.distro_release_attr('codename') \ - or '' - - def info(self, pretty=False, best=False): - """ - Return certain machine-readable information about the OS - distribution. - - For details, see :func:`distro.info`. - """ - return dict( - id=self.id(), - version=self.version(pretty, best), - version_parts=dict( - major=self.major_version(best), - minor=self.minor_version(best), - build_number=self.build_number(best) - ), - like=self.like(), - codename=self.codename(), - ) - - def os_release_info(self): - """ - Return a dictionary containing key-value pairs for the information - items from the os-release file data source of the OS distribution. - - For details, see :func:`distro.os_release_info`. - """ - return self._os_release_info - - def lsb_release_info(self): - """ - Return a dictionary containing key-value pairs for the information - items from the lsb_release command data source of the OS - distribution. - - For details, see :func:`distro.lsb_release_info`. - """ - return self._lsb_release_info - - def distro_release_info(self): - """ - Return a dictionary containing key-value pairs for the information - items from the distro release file data source of the OS - distribution. - - For details, see :func:`distro.distro_release_info`. - """ - return self._distro_release_info - - def uname_info(self): - """ - Return a dictionary containing key-value pairs for the information - items from the uname command data source of the OS distribution. - - For details, see :func:`distro.uname_info`. - """ - return self._uname_info - - def os_release_attr(self, attribute): - """ - Return a single named information item from the os-release file data - source of the OS distribution. - - For details, see :func:`distro.os_release_attr`. - """ - return self._os_release_info.get(attribute, '') - - def lsb_release_attr(self, attribute): - """ - Return a single named information item from the lsb_release command - output data source of the OS distribution. - - For details, see :func:`distro.lsb_release_attr`. - """ - return self._lsb_release_info.get(attribute, '') - - def distro_release_attr(self, attribute): - """ - Return a single named information item from the distro release file - data source of the OS distribution. - - For details, see :func:`distro.distro_release_attr`. - """ - return self._distro_release_info.get(attribute, '') - - def uname_attr(self, attribute): - """ - Return a single named information item from the uname command - output data source of the OS distribution. - - For details, see :func:`distro.uname_release_attr`. - """ - return self._uname_info.get(attribute, '') - - @cached_property - def _os_release_info(self): - """ - Get the information items from the specified os-release file. - - Returns: - A dictionary containing all information items. - """ - if os.path.isfile(self.os_release_file): - with open(self.os_release_file) as release_file: - return self._parse_os_release_content(release_file) - return {} - - @staticmethod - def _parse_os_release_content(lines): - """ - Parse the lines of an os-release file. - - Parameters: - - * lines: Iterable through the lines in the os-release file. - Each line must be a unicode string or a UTF-8 encoded byte - string. - - Returns: - A dictionary containing all information items. - """ - props = {} - lexer = shlex.shlex(lines, posix=True) - lexer.whitespace_split = True - - # The shlex module defines its `wordchars` variable using literals, - # making it dependent on the encoding of the Python source file. - # In Python 2.6 and 2.7, the shlex source file is encoded in - # 'iso-8859-1', and the `wordchars` variable is defined as a byte - # string. This causes a UnicodeDecodeError to be raised when the - # parsed content is a unicode object. The following fix resolves that - # (... but it should be fixed in shlex...): - if sys.version_info[0] == 2 and isinstance(lexer.wordchars, bytes): - lexer.wordchars = lexer.wordchars.decode('iso-8859-1') - - tokens = list(lexer) - for token in tokens: - # At this point, all shell-like parsing has been done (i.e. - # comments processed, quotes and backslash escape sequences - # processed, multi-line values assembled, trailing newlines - # stripped, etc.), so the tokens are now either: - # * variable assignments: var=value - # * commands or their arguments (not allowed in os-release) - if '=' in token: - k, v = token.split('=', 1) - props[k.lower()] = v - else: - # Ignore any tokens that are not variable assignments - pass - - if 'version_codename' in props: - # os-release added a version_codename field. Use that in - # preference to anything else Note that some distros purposefully - # do not have code names. They should be setting - # version_codename="" - props['codename'] = props['version_codename'] - elif 'ubuntu_codename' in props: - # Same as above but a non-standard field name used on older Ubuntus - props['codename'] = props['ubuntu_codename'] - elif 'version' in props: - # If there is no version_codename, parse it from the version - codename = re.search(r'(\(\D+\))|,(\s+)?\D+', props['version']) - if codename: - codename = codename.group() - codename = codename.strip('()') - codename = codename.strip(',') - codename = codename.strip() - # codename appears within paranthese. - props['codename'] = codename - - return props - - @cached_property - def _lsb_release_info(self): - """ - Get the information items from the lsb_release command output. - - Returns: - A dictionary containing all information items. - """ - if not self.include_lsb: - return {} - with open(os.devnull, 'w') as devnull: - try: - cmd = ('lsb_release', '-a') - stdout = subprocess.check_output(cmd, stderr=devnull) - except OSError: # Command not found - return {} - content = self._to_str(stdout).splitlines() - return self._parse_lsb_release_content(content) - - @staticmethod - def _parse_lsb_release_content(lines): - """ - Parse the output of the lsb_release command. - - Parameters: - - * lines: Iterable through the lines of the lsb_release output. - Each line must be a unicode string or a UTF-8 encoded byte - string. - - Returns: - A dictionary containing all information items. - """ - props = {} - for line in lines: - kv = line.strip('\n').split(':', 1) - if len(kv) != 2: - # Ignore lines without colon. - continue - k, v = kv - props.update({k.replace(' ', '_').lower(): v.strip()}) - return props - - @cached_property - def _uname_info(self): - with open(os.devnull, 'w') as devnull: - try: - cmd = ('uname', '-rs') - stdout = subprocess.check_output(cmd, stderr=devnull) - except OSError: - return {} - content = self._to_str(stdout).splitlines() - return self._parse_uname_content(content) - - @staticmethod - def _parse_uname_content(lines): - props = {} - match = re.search(r'^([^\s]+)\s+([\d\.]+)', lines[0].strip()) - if match: - name, version = match.groups() - - # This is to prevent the Linux kernel version from - # appearing as the 'best' version on otherwise - # identifiable distributions. - if name == 'Linux': - return {} - props['id'] = name.lower() - props['name'] = name - props['release'] = version - return props - - @staticmethod - def _to_str(text): - encoding = sys.getfilesystemencoding() - encoding = 'utf-8' if encoding == 'ascii' else encoding - - if sys.version_info[0] >= 3: - if isinstance(text, bytes): - return text.decode(encoding) - else: - if isinstance(text, unicode): # noqa - return text.encode(encoding) - - return text - - @cached_property - def _distro_release_info(self): - """ - Get the information items from the specified distro release file. - - Returns: - A dictionary containing all information items. - """ - if self.distro_release_file: - # If it was specified, we use it and parse what we can, even if - # its file name or content does not match the expected pattern. - distro_info = self._parse_distro_release_file( - self.distro_release_file) - basename = os.path.basename(self.distro_release_file) - # The file name pattern for user-specified distro release files - # is somewhat more tolerant (compared to when searching for the - # file), because we want to use what was specified as best as - # possible. - match = _DISTRO_RELEASE_BASENAME_PATTERN.match(basename) - if 'name' in distro_info \ - and 'cloudlinux' in distro_info['name'].lower(): - distro_info['id'] = 'cloudlinux' - elif match: - distro_info['id'] = match.group(1) - return distro_info - else: - try: - basenames = os.listdir(_UNIXCONFDIR) - # We sort for repeatability in cases where there are multiple - # distro specific files; e.g. CentOS, Oracle, Enterprise all - # containing `redhat-release` on top of their own. - basenames.sort() - except OSError: - # This may occur when /etc is not readable but we can't be - # sure about the *-release files. Check common entries of - # /etc for information. If they turn out to not be there the - # error is handled in `_parse_distro_release_file()`. - basenames = ['SuSE-release', - 'arch-release', - 'base-release', - 'centos-release', - 'fedora-release', - 'gentoo-release', - 'mageia-release', - 'mandrake-release', - 'mandriva-release', - 'mandrivalinux-release', - 'manjaro-release', - 'oracle-release', - 'redhat-release', - 'sl-release', - 'slackware-version'] - for basename in basenames: - if basename in _DISTRO_RELEASE_IGNORE_BASENAMES: - continue - match = _DISTRO_RELEASE_BASENAME_PATTERN.match(basename) - if match: - filepath = os.path.join(_UNIXCONFDIR, basename) - distro_info = self._parse_distro_release_file(filepath) - if 'name' in distro_info: - # The name is always present if the pattern matches - self.distro_release_file = filepath - distro_info['id'] = match.group(1) - if 'cloudlinux' in distro_info['name'].lower(): - distro_info['id'] = 'cloudlinux' - return distro_info - return {} - - def _parse_distro_release_file(self, filepath): - """ - Parse a distro release file. - - Parameters: - - * filepath: Path name of the distro release file. - - Returns: - A dictionary containing all information items. - """ - try: - with open(filepath) as fp: - # Only parse the first line. For instance, on SLES there - # are multiple lines. We don't want them... - return self._parse_distro_release_content(fp.readline()) - except (OSError, IOError): - # Ignore not being able to read a specific, seemingly version - # related file. - # See https://github.com/nir0s/distro/issues/162 - return {} - - @staticmethod - def _parse_distro_release_content(line): - """ - Parse a line from a distro release file. - - Parameters: - * line: Line from the distro release file. Must be a unicode string - or a UTF-8 encoded byte string. - - Returns: - A dictionary containing all information items. - """ - matches = _DISTRO_RELEASE_CONTENT_REVERSED_PATTERN.match( - line.strip()[::-1]) - distro_info = {} - if matches: - # regexp ensures non-None - distro_info['name'] = matches.group(3)[::-1] - if matches.group(2): - distro_info['version_id'] = matches.group(2)[::-1] - if matches.group(1): - distro_info['codename'] = matches.group(1)[::-1] - elif line: - distro_info['name'] = line.strip() - return distro_info - - -_distro = LinuxDistribution() - - -ABI_CC = "abi-compliance-checker" -ABI_DUMPER = "abi-dumper" - -CMD_NAME = os.path.basename(__file__) -ERROR_CODE = {"Ok": 0, "Error": 1, "Empty": 10, "NoDebug": 11, "NoABI": 12} - -ARGS = {} - - -def run_cmd(cmd): - ''' - run command in subprocess and return exit code, output, error. - ''' - os.putenv('LANG', 'C') - os.putenv('LC_ALL', 'C') - os.environ['LANG'] = 'C' - os.environ['LC_ALL'] = 'C' - cmd = cmd.encode('UTF-8') - proc = subprocess.Popen(cmd, shell=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - (stdout, stderr) = proc.communicate() - returncode = proc.returncode - return (returncode, stdout, stderr) - - -def parse_args(): - desc = "Collect some infomation about bin file." - parser = argparse.ArgumentParser( - description=desc, epilog=f"example: {CMD_NAME} -bin /usr/bin/openssh") - parser.add_argument('-v', action='version', - version='Package ABI Info Collector '+TOOL_VERSION) - parser.add_argument( - '-debug', help='enable debug messages', action='store_true') - parser.add_argument('-bin', metavar='BINFILE', - help='collect binary abi info',) - parser.add_argument( - '-debuginfo', help=argparse.SUPPRESS, action='store_true') - parser.add_argument( - '-export-dir', help='specify a directory to save and reuse ABI info export (default: ./abi-info-export)', metavar='DIR') - #parser.add_argument('-src', help='collect source abi info', action='store_true') - return parser.parse_args() - - -def collect_os_info(export_dir): - shutil.copy('/etc/os-release', f'{export_dir}/OLD-os-release.info') - - -def collect_name_info(name, export_dir): - - outfile = f'{export_dir}/OLD-name.info' - with open(f'{outfile}', 'wb+') as f: - f.write(name.encode()) - - -def collect_readelf_info(binfile, export_dir): - - print('Checking information about ELF ...') - # eu-readelf is better than readelf - cmd = "eu-readelf -s " + binfile - returncode, stdout, stderr = run_cmd(cmd) - - if returncode != 0: - exit_status('Error', f'read {binfile} elf info failed') - - outfile = f'{export_dir}/OLD-elf.info' - with open(f'{outfile}', 'wb+') as f: - f.write(stdout) - - return outfile - - -def collect_ldd_info(binfile, export_dir): - - print('Checking shared object dependencies ...') - cmd = "ldd -v " + binfile - returncode, stdout, stderr = run_cmd(cmd) - - if returncode != 0: - exit_status('Error', f'read {binfile} ldd info failed') - - outfile = f'{export_dir}/OLD-ldd.info' - with open(f'{outfile}', 'wb+') as f: - f.write(stdout) - - return outfile - - -def find_soname_file(f_elf, f_ldd, export_dir): - print('Checking package dependencies ...') - sym_set = set() - func_sym_set = set() - with open(f'{f_elf}', 'rt') as f: - elf_symbol_fmt = ' *(?P[0-9]*): (?P[0-9abcdef]*) (?P[0-9]*).*(FUNC).*@.*' - for line in f: - m = re.match(elf_symbol_fmt, line) - if not m: - continue - elf_line_list = re.split(r'\s+', line.strip()) - sym = elf_line_list[7].split('@') - func_sym_set.add(sym[0]) - sym_set.add(sym[1]) - if ARGS.debug: - print(f"All elf syms is {func_sym_set}") - - file = f"{export_dir}/OLD-func-syms.info" - with open(f'{file}', 'wb+') as f: - for line in func_sym_set: - f.write((line+'\n').encode()) - - soname_file_set = set() - with open(f'{f_ldd}', 'rt') as f: - for line in f: - for sym in sym_set: - # libcrypto.so.10 => /lib64/libcrypto.so.10 (0x00007f9327355000) - if not re.match(' *(?P[a-zA-Z]*).*\).*=>.*', line): - continue - # libdl.so.2 (GLIBC_2.2.5) => /lib64/libdl.so.2 - if not re.search(f'({sym})', line): - continue - soname_file_set.add(line.strip().split("=>")[1]) - return soname_file_set - - -def find_so_rpm_pkgs_name(soname_file_set, export_dir): - so_pkgs_name_set = set() - - for file in soname_file_set: - cmd = (f'rpm -qf {file} ' + ' --qf "%{name}\n"') - returncode, stdout, stderr = run_cmd(cmd) - if returncode != 0: - logging.error(f"return code: {returncode}, {stderr.decode()}") - continue - so_pkgs_name_set.add(stdout) - - outfile = f'{export_dir}/OLD-pkgs.info' - with open(f'{outfile}', 'wb+') as f: - for rpm_name in so_pkgs_name_set: - f.write(rpm_name) - - -def find_soname_package(soname_file_set, export_dir): - os_distro = _distro.linux_distribution(full_distribution_name=False)[0] - if os_distro == 'debain': - pass - if os_distro == 'centos' or os_distro == 'fedora': - find_so_rpm_pkgs_name(soname_file_set, export_dir) - - -def compress_outfile(binfile, export_dir): - - tar_file = f'{export_dir}/OLD-abi-info.tar.gz' - with tarfile.open(tar_file, "w:gz") as tar: - for parent, dirnames, filenames in os.walk(f'{export_dir}'): - filenames[:] = [f for f in filenames if f.endswith( - (".info", '.xml', '.dump'))] - for filename in filenames: - pathfile = os.path.join(parent, filename) - # 去除 tarball 中的父目录 - tar.add(pathfile, arcname=filename) - os.remove(pathfile) - tar_path = os.path.realpath(tar_file) - print(f'{binfile} information: {tar_path}') - - -def s_exit(code): - sys.exit(ERROR_CODE[code]) - - -def print_err(msg): - sys.stderr.write(msg+"\n") - - -def exit_status(code, msg): - if code != "Ok": - print_err("ERROR: "+msg) - else: - print(msg) - - s_exit(code) - - -def check_cmd(prog): - for path in os.environ['PATH'].split(os.pathsep): - path = path.strip('"') - candidate = path+"/"+prog - if os.path.isfile(candidate) and os.access(candidate, os.X_OK): - return candidate - return None - - -def get_rpmname_without_libs(x): - # 正则获取rpmname - return re.sub('-libs$', '', x).strip() - - -def find_devel_lib_package(target_path): - print('Checking the dependency ...') - file = f'{target_path}/OLD-pkgs.info' - if not os.access(f'{file}', os.R_OK): - exit_status( - "Error", f"The info file {file} can't be read. Please check") - - dev_pkgs_set = set() - libs_pkgs_set = set() - dbginfo_pkgs_set = set() - with open(file, 'r') as f: - for line in f: - rpm_name = get_rpmname_without_libs(line) - dev_pkgs_set.add(f'{rpm_name}-devel') - dbginfo_pkgs_set.add(f'{rpm_name}-debuginfo') - # 包名中包含 lib 关键字的包,本身就是 lib 包 - if 'lib' not in rpm_name: - libs_pkgs_set.add(f'{rpm_name}-libs') - else: - libs_pkgs_set.add(f'{rpm_name}') - return dev_pkgs_set, libs_pkgs_set, dbginfo_pkgs_set - - -def try_install_packages(dev_pkgs, libs_pkgs=set()): - not_installed_pkgs = set() - - dev_pkgs = set(dev_pkgs) | set(libs_pkgs) - for pkg in dev_pkgs: - cmd = f'rpm -q {pkg}' - returncode, stdout, stderr = run_cmd(cmd) - if returncode != 0: - print(f"It seems that the OS doesn't install {pkg}") - not_installed_pkgs.add(pkg) - else: - print(f'The packages "{pkg}" have been installed') - - install_failed_pkgs = set() - for pkg in not_installed_pkgs: - cmd = f'yum install -y {pkg}' - print(f'Trying to install package {pkg} with yum') - returncode, stdout, stderr = run_cmd(cmd) - if returncode != 0: - print(f"Can't install {pkg}, with yum") - install_failed_pkgs.add(pkg) - else: - print(f'Successfully installed {pkg} with yum') - - if install_failed_pkgs: - exit_status( - "Error", f'Please install {install_failed_pkgs}, then retry') - - -def gen_xml_and_dump(basename, target_path, dev_pkgs_set, libs_files_set): - headers_list = list() - for pkg in dev_pkgs_set: - cmd = f'rpm -ql {pkg} | grep .*include.*\.h$' - returncode, stdout, stderr = run_cmd(cmd) - headers_list.append(stdout.decode()) - - file = f'{target_path}/OLD-dump.xml' - with open(file, 'wt+') as f: - f.write("\n") - f.write('1.0\n') - f.write('\n') - - f.write("\n") - f.writelines(headers_list) - f.write("\n") - - f.write("\n") - for line in libs_files_set: - f.write(line+'\n') - f.write("\n") - dump_file = f'{target_path}/OLD-abi.dump' - cmd = f'abi-compliance-checker -xml -l {basename} -dump {file} -dump-path {dump_file}' - print(f'Analyzing the symbols ...') - returncode, stdout, stderr = run_cmd(cmd) - - return dump_file - - -def gen_dump_with_debuginfo(basename, export_dir, debuginfo_pkgs): - # TODO:当前实现有困难 - pass - - -def main(): - - global ARGS - ARGS = parse_args() - - # 检查参数 - if not ARGS.bin: - exit_status('Error', 'bin file are not specified (-bin option)') - - binfile = ARGS.bin - if not os.path.isfile(binfile): - exit_status('Error', f'''file "{binfile}" does not exist.''') - if not os.access(binfile, os.R_OK): - exit_status('Error', f'''file "{binfile}" can't be read.''') - - textchars = bytearray({7, 8, 9, 10, 12, 13, 27} | - set(range(0x20, 0x100)) - {0x7f}) - - def is_binary_string(bytes): return bool(bytes.translate(None, textchars)) - - if not is_binary_string(open(binfile, 'rb').read(1024)): - exit_status( - 'Error', f'''file {binfile} isn't a standard binary file.''') - - if ARGS.export_dir: - export_dir = ARGS.export_dir - else: - export_dir = "abi-info-export" - basename = os.path.basename(binfile) - export_dir += "/" + basename - if not os.path.exists(export_dir): - os.makedirs(export_dir) - - # 收集 basename 到 export_dir 目录下 - collect_name_info(basename, export_dir) - - # 检查 abi-compliance-checker 是否安装 - global ABI_CC, ABI_DUMPER - if not check_cmd(ABI_CC): - exit_status('Error', 'ABI Compliance Checker is not installed') - - # 检查 abi-dumper 是否安装 - if not check_cmd(ABI_DUMPER): - exit_status('Error', 'ABI Dumper is not installed') - - # 收集 os-release 到 export_dir 目录下 - collect_os_info(export_dir) - - # 收集 elf 信息到 export_dir 目录下 - f_elf = collect_readelf_info(binfile, export_dir) - - # 收集 ldd 信息到 export_dir 目录下 - f_ldd = collect_ldd_info(binfile, export_dir) - - # 通过 elf 信息和 ldd 信息查找 /lib64/libxxx.so.xx 集合 - soname_file_set = find_soname_file(f_elf, f_ldd, export_dir) - if ARGS.debug: - print(f"The dynamic libraries used by the bin are {soname_file_set}") - - # 通过 /lib64/libxxx.so.xx 集合, 分析软件包名信息到 export_dir 目录下 - find_soname_package(soname_file_set, export_dir) - - # 解析 dev 包 和 lib 包 - dev_pkgs, libs_pkgs, debuginfo_pkgs = find_devel_lib_package(export_dir) - - if not ARGS.debuginfo: - - # 判断系统中是否安装 dev 包 和 lib 包 - try_install_packages(dev_pkgs, libs_pkgs) - # 生成 abi-compliance-checker 用到的 xml 文件,并生成 dump 文件 - gen_xml_and_dump( - basename, export_dir, dev_pkgs, soname_file_set) - else: - # 判断系统中是否安装 debuginfo_pkgs 包 - try_install_packages(debuginfo_pkgs) - # 通过 debuginfo 信息生成 dump 文件 - gen_dump_with_debuginfo(basename, export_dir, - debuginfo_pkgs) - - # 输出 tarball 到 export_dir 目录下 - compress_outfile(binfile, export_dir) - - -if __name__ == "__main__": - main() diff --git a/src/binhandler.py b/src/binhandler.py index 9d0b56925a40ae8d7400e06b492f1bb75f5793e2..3b54168625642dbcdf8f827fea6bba35070e1cda 100644 --- a/src/binhandler.py +++ b/src/binhandler.py @@ -2,7 +2,6 @@ import logging import os import platform import re -import shutil import distro @@ -21,7 +20,7 @@ CONVERT = "convert" class ABI: def __init__(self): self.logger = logging.getLogger(__name__) - self.output_dir = tool_opts.output_dir + self.export_dir = tool_opts.export_dir self.binfile = os.path.abspath(tool_opts.binfile) self.basename = os.path.basename(self.binfile) @@ -84,14 +83,14 @@ class ABI: f"{READELF} -s {self.binfile}", print_output=False ) - output_file = os.path.join(self.output_dir, self.READELF_FILE) + output_file = os.path.join(self.export_dir, self.READELF_FILE) utils.store_content_to_file(output_file, output) def gen_ldd_info(self): self.logger.info(f"Checking ldd information of file {self.binfile} ...") output, _ = utils.run_subprocess(f"{LDD} {self.binfile}", print_output=False) - output_file = os.path.join(self.output_dir, self.LDD_FILE) + output_file = os.path.join(self.export_dir, self.LDD_FILE) utils.store_content_to_file(output_file, output) def gen_soname_file(self): @@ -102,7 +101,7 @@ class ABI: func_dynsym_name_list = list() # list lke [ "OPENSSL_1_1_0" ] func_dynsym_ver_list = list() - elf_file = os.path.join(self.output_dir, self.READELF_FILE) + elf_file = os.path.join(self.export_dir, self.READELF_FILE) elf_text = utils.get_file_content(elf_file, as_list=True) elf_symbol_fmt = ( " *(?P[0-9]*): (?P[0-9abcdef]*) (?P[0-9]*).*(FUNC).*@.*" @@ -123,19 +122,19 @@ class ABI: if sym[1] not in func_dynsym_ver_list: func_dynsym_ver_list.append(sym[1]) - output_file = os.path.join(self.output_dir, self.FUNC_DYNSYM_FILE) + output_file = os.path.join(self.export_dir, self.FUNC_DYNSYM_FILE) self.logger.info(f"Writing file {output_file} ...") utils.store_content_to_file(output_file, func_dynsym_list) - output_file = os.path.join(self.output_dir, self.FUNC_DYNSYM_NAME_FILE) + output_file = os.path.join(self.export_dir, self.FUNC_DYNSYM_NAME_FILE) self.logger.info(f"Writing file {output_file} ...") utils.store_content_to_file(output_file, func_dynsym_name_list) - output_file = os.path.join(self.output_dir, self.FUNC_DYNSYM_VER_FILE) + output_file = os.path.join(self.export_dir, self.FUNC_DYNSYM_VER_FILE) self.logger.info(f"Writing file {output_file} ...") utils.store_content_to_file(output_file, func_dynsym_ver_list) - ldd_file = os.path.join(self.output_dir, self.LDD_FILE) + ldd_file = os.path.join(self.export_dir, self.LDD_FILE) ldd_text = utils.get_file_content(ldd_file, as_list=True) soname_file_list = list() @@ -415,7 +414,7 @@ class ABI: f.write("\n") def gen_old_xml(self): - file = os.path.join(self.output_dir, self.OLD_XML_FILE) + file = os.path.join(self.export_dir, self.OLD_XML_FILE) os_version = self.old_os_full_name dev_pkgs = self.old_required_rpm_devel_pkgs libs_pkgs = self.old_required_rpm_pkgs @@ -435,7 +434,7 @@ class ABI: ) def gen_new_xml(self): - file = os.path.join(self.output_dir, self.NEW_XML_FILE) + file = os.path.join(self.export_dir, self.NEW_XML_FILE) os_version = self.new_os_full_name dev_pkgs = self.new_required_rpm_devel_pkgs libs_pkgs = self.new_required_rpm_pkgs + self.new_required_rpm_libs_pkgs @@ -470,9 +469,9 @@ class ABI: name = self.basename num = self.old_os_full_name - xml_file = os.path.join(self.output_dir, self.OLD_XML_FILE) - dump_file = os.path.join(self.output_dir, self.OLD_DUMP_FILE) - log_file = os.path.join(self.output_dir, self.OLD_ABICC_LOG) + xml_file = os.path.join(self.export_dir, self.OLD_XML_FILE) + dump_file = os.path.join(self.export_dir, self.OLD_DUMP_FILE) + log_file = os.path.join(self.export_dir, self.OLD_ABICC_LOG) self._gen_dump(name, num, xml_file, dump_file, log_file) @@ -480,20 +479,20 @@ class ABI: name = self.basename num = self.new_os_full_name - xml_file = os.path.join(self.output_dir, self.NEW_XML_FILE) - dump_file = os.path.join(self.output_dir, self.NEW_DUMP_FILE) - log_file = os.path.join(self.output_dir, self.NEW_ABICC_LOG) + xml_file = os.path.join(self.export_dir, self.NEW_XML_FILE) + dump_file = os.path.join(self.export_dir, self.NEW_DUMP_FILE) + log_file = os.path.join(self.export_dir, self.NEW_ABICC_LOG) self._gen_dump(name, num, xml_file, dump_file, log_file) def diff_dump(self): name = self.basename - new_dump = os.path.join(self.output_dir, self.NEW_DUMP_FILE) + new_dump = os.path.join(self.export_dir, self.NEW_DUMP_FILE) - old_dump = os.path.join(self.output_dir, self.OLD_DUMP_FILE) - log_file = os.path.join(self.output_dir, self.DIFF_ABICC_LOG) - sym_list = os.path.join(self.output_dir, self.FUNC_DYNSYM_NAME_FILE) - html = os.path.join(self.output_dir, self.EXPORT_HTML_FILE) + old_dump = os.path.join(self.export_dir, self.OLD_DUMP_FILE) + log_file = os.path.join(self.export_dir, self.DIFF_ABICC_LOG) + sym_list = os.path.join(self.export_dir, self.FUNC_DYNSYM_NAME_FILE) + html = os.path.join(self.export_dir, self.EXPORT_HTML_FILE) self.logger.info(f"Comparing dump file {old_dump} and {new_dump} ...") cmd = f"{ABI_CC} -l {name}" @@ -508,8 +507,8 @@ class ABI: def gen_soname_deppng(self): - png_file = os.path.join(self.output_dir, self.OLD_SO_PNG_FILE) - dot_file = os.path.join(self.output_dir, self.OLD_SO_DOT_FILE) + png_file = os.path.join(self.export_dir, self.OLD_SO_PNG_FILE) + dot_file = os.path.join(self.export_dir, self.OLD_SO_DOT_FILE) file = self.binfile self.logger.info( f"The so running dependency graph for {file} is being generated..." @@ -551,8 +550,8 @@ class ABI: def gen_rpm_deppng(self): - png_file = os.path.join(self.output_dir, self.OLD_RPM_PNG_FILE) - dot_file = os.path.join(self.output_dir, self.OLD_RPM_DOT_FILE) + png_file = os.path.join(self.export_dir, self.OLD_RPM_PNG_FILE) + dot_file = os.path.join(self.export_dir, self.OLD_RPM_DOT_FILE) file = self.binfile self.logger.info( f"The rpm running dependency graph for {file} is being generated..." @@ -607,7 +606,7 @@ class ABI: def add_deptab(self): - html_file = os.path.join(self.output_dir, self.EXPORT_HTML_FILE) + html_file = os.path.join(self.export_dir, self.EXPORT_HTML_FILE) self.logger.info(f"Generating html file {html_file}") substr = r"\n" @@ -636,14 +635,9 @@ class ABI: self.logger.info(f"Complete generation.") def show_html(self): - html_file = os.path.join(self.output_dir, self.EXPORT_HTML_FILE) + html_file = os.path.join(self.export_dir, self.EXPORT_HTML_FILE) self.logger.info(f"The check result is {os.path.abspath(html_file)}") - @staticmethod - def clean_cache(): - cache_dir = '/tmp/abi-info-check-cache' - if os.path.exists(cache_dir): - shutil.rmtree(cache_dir) # get the libs prog depends on and write the results into opened file f analyzedlist = [] diff --git a/src/conf/aarch64/centos_7.1.conf b/src/conf/aarch64/centos_7.1.conf new file mode 100644 index 0000000000000000000000000000000000000000..6ab6ce079f1bf073ae3738c5314f352ee377943d --- /dev/null +++ b/src/conf/aarch64/centos_7.1.conf @@ -0,0 +1,22 @@ +[main] +gpgcheck=0 +installonly_limit=3 +clean_requirements_on_remove=True +best=True +skip_if_unavailable=False +enabled=1 +cachedir=/tmp +reposdir=/dev/null + +[huaweicloud] +name = CentOS Linux 7.1.1503 +baseurl = https://mirrors.huaweicloud.com/centos-vault/altarch/7.1.1503/os/aarch64/ + + +# [tuna] +# name = CentOS Linux 7.1.1503 +# baseurl = https://mirrors.tuna.tsinghua.edu.cn/centos-vault/altarch/7.1.1503/os/aarch64/ + +# [debuginfo] +# name = debuginfo +# baseurl = http://debuginfo.centos.org/7/aarch64/ \ No newline at end of file diff --git a/src/conf/aarch64/centos_7.2.conf b/src/conf/aarch64/centos_7.2.conf new file mode 100644 index 0000000000000000000000000000000000000000..bdb72113fe85cd6790247c52be4784a70acd794b --- /dev/null +++ b/src/conf/aarch64/centos_7.2.conf @@ -0,0 +1,22 @@ +[main] +gpgcheck=0 +installonly_limit=3 +clean_requirements_on_remove=True +best=True +skip_if_unavailable=False +enabled=1 +cachedir=/tmp +reposdir=/dev/null + +[huaweicloud] +name = CentOS Linux 7.2.1603 +baseurl = https://mirrors.huaweicloud.com/centos-vault/altarch/7.2.1603/os/aarch64/ + + +# [tuna] +# name = CentOS Linux 7.2.1603 +# baseurl = https://mirrors.tuna.tsinghua.edu.cn/centos-vault/altarch/7.2.1603/os/aarch64/ + +# [debuginfo] +# name = debuginfo +# baseurl = http://debuginfo.centos.org/7/aarch64/ \ No newline at end of file diff --git a/src/conf/aarch64/centos_7.3.conf b/src/conf/aarch64/centos_7.3.conf new file mode 100644 index 0000000000000000000000000000000000000000..4dc1a1511afbedf1520d732eddadd5a024d0c9f7 --- /dev/null +++ b/src/conf/aarch64/centos_7.3.conf @@ -0,0 +1,22 @@ +[main] +gpgcheck=0 +installonly_limit=3 +clean_requirements_on_remove=True +best=True +skip_if_unavailable=False +enabled=1 +cachedir=/tmp +reposdir=/dev/null + +[huaweicloud] +name = CentOS Linux 7.3.1611 +baseurl = https://mirrors.huaweicloud.com/centos-vault/altarch/7.3.1611/os/aarch64/ + + +# [tuna] +# name = CentOS Linux 7.3.1611 +# baseurl = https://mirrors.tuna.tsinghua.edu.cn/centos-vault/altarch/7.3.1611/os/aarch64/ + +# [debuginfo] +# name = debuginfo +# baseurl = http://debuginfo.centos.org/7/aarch64/ \ No newline at end of file diff --git a/src/conf/aarch64/centos_7.4.conf b/src/conf/aarch64/centos_7.4.conf new file mode 100644 index 0000000000000000000000000000000000000000..6d0cd59d0072d7f5c1965f2881a2ec93dc41ad7b --- /dev/null +++ b/src/conf/aarch64/centos_7.4.conf @@ -0,0 +1,22 @@ +[main] +gpgcheck=0 +installonly_limit=3 +clean_requirements_on_remove=True +best=True +skip_if_unavailable=False +enabled=1 +cachedir=/tmp +reposdir=/dev/null + +[huaweicloud] +name = CentOS Linux 7.4.1708 +baseurl = https://mirrors.huaweicloud.com/centos-vault/altarch/7.4.1708/os/aarch64/ + + +# [tuna] +# name = CentOS Linux 7.4.1708 +# baseurl = https://mirrors.tuna.tsinghua.edu.cn/centos-vault/altarch/7.4.1708/os/aarch64/ + +# [debuginfo] +# name = debuginfo +# baseurl = http://debuginfo.centos.org/7/aarch64/ \ No newline at end of file diff --git a/src/conf/aarch64/centos_7.5.conf b/src/conf/aarch64/centos_7.5.conf new file mode 100644 index 0000000000000000000000000000000000000000..4602df520c2f1c19554877a414cff1030a4c5214 --- /dev/null +++ b/src/conf/aarch64/centos_7.5.conf @@ -0,0 +1,22 @@ +[main] +gpgcheck=0 +installonly_limit=3 +clean_requirements_on_remove=True +best=True +skip_if_unavailable=False +enabled=1 +cachedir=/tmp +reposdir=/dev/null + +[huaweicloud] +name = CentOS Linux 7.5.1804 +baseurl = https://mirrors.huaweicloud.com/centos-vault/altarch/7.5.1804/os/aarch64/ + + +# [tuna] +# name = CentOS Linux 7.5.1804 +# baseurl = https://mirrors.tuna.tsinghua.edu.cn/centos-vault/altarch/7.5.1804/os/aarch64/ + +# [debuginfo] +# name = debuginfo +# baseurl = http://debuginfo.centos.org/7/aarch64/ \ No newline at end of file diff --git a/src/conf/aarch64/centos_7.6.conf b/src/conf/aarch64/centos_7.6.conf new file mode 100644 index 0000000000000000000000000000000000000000..b22546bb14b554d009969bb3c818a145d2f433eb --- /dev/null +++ b/src/conf/aarch64/centos_7.6.conf @@ -0,0 +1,22 @@ +[main] +gpgcheck=0 +installonly_limit=3 +clean_requirements_on_remove=True +best=True +skip_if_unavailable=False +enabled=1 +cachedir=/tmp +reposdir=/dev/null + +[huaweicloud] +name = CentOS Linux 7.6.1810 +baseurl = https://mirrors.huaweicloud.com/centos-vault/altarch/7.6.1810/os/aarch64/ + + +# [tuna] +# name = CentOS Linux 7.6.1810 +# baseurl = https://mirrors.tuna.tsinghua.edu.cn/centos-vault/altarch/7.6.1810/os/aarch64/ + +# [debuginfo] +# name = debuginfo +# baseurl = http://debuginfo.centos.org/7/aarch64/ \ No newline at end of file diff --git a/src/conf/aarch64/centos_7.7.conf b/src/conf/aarch64/centos_7.7.conf new file mode 100644 index 0000000000000000000000000000000000000000..39c8fe3a492aa88528d7c73b497806ec9685e7f5 --- /dev/null +++ b/src/conf/aarch64/centos_7.7.conf @@ -0,0 +1,22 @@ +[main] +gpgcheck=0 +installonly_limit=3 +clean_requirements_on_remove=True +best=True +skip_if_unavailable=False +enabled=1 +cachedir=/tmp +reposdir=/dev/null + +[huaweicloud] +name = CentOS Linux 7.7.1908 +baseurl = https://mirrors.huaweicloud.com/centos-vault/altarch/7.7.1908/os/aarch64/ + + +# [tuna] +# name = CentOS Linux 7.7.1908 +# baseurl = https://mirrors.tuna.tsinghua.edu.cn/centos-vault/altarch/7.7.1908/os/aarch64/ + +# [debuginfo] +# name = debuginfo +# baseurl = http://debuginfo.centos.org/7/aarch64/ \ No newline at end of file diff --git a/src/conf/aarch64/centos_7.8.conf b/src/conf/aarch64/centos_7.8.conf new file mode 100644 index 0000000000000000000000000000000000000000..6f584b55ae1100ac7ce78d36a1c589faaae33102 --- /dev/null +++ b/src/conf/aarch64/centos_7.8.conf @@ -0,0 +1,22 @@ +[main] +gpgcheck=0 +installonly_limit=3 +clean_requirements_on_remove=True +best=True +skip_if_unavailable=False +enabled=1 +cachedir=/tmp +reposdir=/dev/null + +[huaweicloud] +name = CentOS Linux 7.8.2003 +baseurl = https://mirrors.huaweicloud.com/centos-vault/altarch/7.8.2003/os/aarch64/ + + +# [tuna] +# name = CentOS Linux 7.8.2003 +# baseurl = https://mirrors.tuna.tsinghua.edu.cn/centos-vault/altarch/7.8.2003/os/aarch64/ + +# [debuginfo] +# name = debuginfo +# baseurl = http://debuginfo.centos.org/7/aarch64/ \ No newline at end of file diff --git a/src/conf/aarch64/centos_7.9.conf b/src/conf/aarch64/centos_7.9.conf new file mode 100644 index 0000000000000000000000000000000000000000..f728e76277bb3a2b61c90fadf6902e26ed9be4a9 --- /dev/null +++ b/src/conf/aarch64/centos_7.9.conf @@ -0,0 +1,22 @@ +[main] +gpgcheck=0 +installonly_limit=3 +clean_requirements_on_remove=True +best=True +skip_if_unavailable=False +enabled=1 +cachedir=/tmp +reposdir=/dev/null + +[huaweicloud] +name = CentOS Linux 7.9.2009 +baseurl = https://mirrors.huaweicloud.com/centos-vault/altarch/7.9.2009/os/aarch64/ + + +# [tuna] +# name = CentOS Linux 7.9.2009 +# baseurl = https://mirrors.tuna.tsinghua.edu.cn/centos-vault/altarch/7.9.2009/os/aarch64/ + +# [debuginfo] +# name = debuginfo +# baseurl = http://debuginfo.centos.org/7/aarch64/ \ No newline at end of file diff --git a/src/conf/x86_64/centos_7.0.conf b/src/conf/x86_64/centos_7.0.conf new file mode 100644 index 0000000000000000000000000000000000000000..4bff37c1929eddfc9846a5aa5395004b2777cdb7 --- /dev/null +++ b/src/conf/x86_64/centos_7.0.conf @@ -0,0 +1,22 @@ +[main] +gpgcheck=0 +installonly_limit=3 +clean_requirements_on_remove=True +best=True +skip_if_unavailable=False +enabled=1 +cachedir=/tmp +reposdir=/dev/null + +[huaweicloud] +name = CentOS Linux 7.0 +baseurl = https://repo.huaweicloud.com/centos-vault/7.0.1406/os/x86_64/ + + +# [tuna] +# name = CentOS Linux 7.0 +# baseurl = https://mirrors.tuna.tsinghua.edu.cn/centos-vault/7.0.1406/os/x86_64/ + +# [debuginfo] +# name = debuginfo +# baseurl = http://debuginfo.centos.org/7/x86_64/ \ No newline at end of file diff --git a/src/conf/x86_64/centos_7.1.conf b/src/conf/x86_64/centos_7.1.conf new file mode 100644 index 0000000000000000000000000000000000000000..e8448122d69283de7ad3945fff22d7d214dcdf46 --- /dev/null +++ b/src/conf/x86_64/centos_7.1.conf @@ -0,0 +1,22 @@ +[main] +gpgcheck=0 +installonly_limit=3 +clean_requirements_on_remove=True +best=True +skip_if_unavailable=False +enabled=1 +cachedir=/tmp +reposdir=/dev/null + +[huaweicloud] +name = CentOS Linux 7.1 +baseurl = https://repo.huaweicloud.com/centos-vault/7.1.1503/os/x86_64/ + + +# [tuna] +# name = CentOS Linux 7.1 +# baseurl = https://mirrors.tuna.tsinghua.edu.cn/centos-vault/7.1.1503/os/x86_64/ + +# [debuginfo] +# name = debuginfo +# baseurl = http://debuginfo.centos.org/7/x86_64/ \ No newline at end of file diff --git a/src/conf/x86_64/centos_7.2.conf b/src/conf/x86_64/centos_7.2.conf new file mode 100644 index 0000000000000000000000000000000000000000..7705878dba2b1fa8e2c01faf8d7838c2a034124c --- /dev/null +++ b/src/conf/x86_64/centos_7.2.conf @@ -0,0 +1,22 @@ +[main] +gpgcheck=0 +installonly_limit=3 +clean_requirements_on_remove=True +best=True +skip_if_unavailable=False +enabled=1 +cachedir=/tmp +reposdir=/dev/null + +[huaweicloud] +name = CentOS Linux 7.2 +baseurl = https://repo.huaweicloud.com/centos-vault/7.2.1511/os/x86_64/ + + +# [tuna] +# name = CentOS Linux 7.2 +# baseurl = https://mirrors.tuna.tsinghua.edu.cn/centos-vault/7.2.1511/os/x86_64/ + +# [debuginfo] +# name = debuginfo +# baseurl = http://debuginfo.centos.org/7/x86_64/ \ No newline at end of file diff --git a/src/conf/x86_64/centos_7.3.conf b/src/conf/x86_64/centos_7.3.conf new file mode 100644 index 0000000000000000000000000000000000000000..46e66e9c39ac498763be0f2c76cbd50f2ecaafed --- /dev/null +++ b/src/conf/x86_64/centos_7.3.conf @@ -0,0 +1,22 @@ +[main] +gpgcheck=0 +installonly_limit=3 +clean_requirements_on_remove=True +best=True +skip_if_unavailable=False +enabled=1 +cachedir=/tmp +reposdir=/dev/null + +[huaweicloud] +name = CentOS Linux 7.3 +baseurl = https://repo.huaweicloud.com/centos-vault/7.3.1611/os/x86_64/ + + +# [tuna] +# name = CentOS Linux 7.3 +# baseurl = https://mirrors.tuna.tsinghua.edu.cn/centos-vault/7.3.1611/os/x86_64/ + +# [debuginfo] +# name = debuginfo +# baseurl = http://debuginfo.centos.org/7/x86_64/ \ No newline at end of file diff --git a/src/conf/x86_64/centos_7.4.conf b/src/conf/x86_64/centos_7.4.conf new file mode 100644 index 0000000000000000000000000000000000000000..45a18d243b87753ada9d11f971d29d30507bb306 --- /dev/null +++ b/src/conf/x86_64/centos_7.4.conf @@ -0,0 +1,22 @@ +[main] +gpgcheck=0 +installonly_limit=3 +clean_requirements_on_remove=True +best=True +skip_if_unavailable=False +enabled=1 +cachedir=/tmp +reposdir=/dev/null + +[huaweicloud] +name = CentOS Linux 7.4 +baseurl = https://repo.huaweicloud.com/centos-vault/7.4.1708/os/x86_64/ + + +# [tuna] +# name = CentOS Linux 7.4 +# baseurl = https://mirrors.tuna.tsinghua.edu.cn/centos-vault/7.4.1708/os/x86_64/ + +# [debuginfo] +# name = debuginfo +# baseurl = http://debuginfo.centos.org/7/x86_64/ \ No newline at end of file diff --git a/src/conf/x86_64/centos_7.5.conf b/src/conf/x86_64/centos_7.5.conf new file mode 100644 index 0000000000000000000000000000000000000000..279a76d39e7d9724138cabfff4d2f39d5d907fb5 --- /dev/null +++ b/src/conf/x86_64/centos_7.5.conf @@ -0,0 +1,22 @@ +[main] +gpgcheck=0 +installonly_limit=3 +clean_requirements_on_remove=True +best=True +skip_if_unavailable=False +enabled=1 +cachedir=/tmp +reposdir=/dev/null + +[huaweicloud] +name = CentOS Linux 7.5 +baseurl = https://repo.huaweicloud.com/centos-vault/7.5.1804/os/x86_64/ + + +# [tuna] +# name = CentOS Linux 7.5 +# baseurl = https://mirrors.tuna.tsinghua.edu.cn/centos-vault/7.5.1804/os/x86_64/ + +# [debuginfo] +# name = debuginfo +# baseurl = http://debuginfo.centos.org/7/x86_64/ \ No newline at end of file diff --git a/src/conf/x86_64/centos_7.6.conf b/src/conf/x86_64/centos_7.6.conf new file mode 100644 index 0000000000000000000000000000000000000000..539cf54a45938aa2927bfc79e7e8cd1075023429 --- /dev/null +++ b/src/conf/x86_64/centos_7.6.conf @@ -0,0 +1,22 @@ +[main] +gpgcheck=0 +installonly_limit=3 +clean_requirements_on_remove=True +best=True +skip_if_unavailable=False +enabled=1 +cachedir=/tmp +reposdir=/dev/null + +[huaweicloud] +name = CentOS Linux 7.6 +baseurl = https://repo.huaweicloud.com/centos-vault/7.6.1810/os/x86_64/ + + +# [tuna] +# name = CentOS Linux 7.6 +# baseurl = https://mirrors.tuna.tsinghua.edu.cn/centos-vault/7.6.1810/os/x86_64/ + +# [debuginfo] +# name = debuginfo +# baseurl = http://debuginfo.centos.org/7/x86_64/ \ No newline at end of file diff --git a/src/conf/x86_64/centos_7.7.conf b/src/conf/x86_64/centos_7.7.conf new file mode 100644 index 0000000000000000000000000000000000000000..ce800fc98588e01a328c82ada086389c70b6eea5 --- /dev/null +++ b/src/conf/x86_64/centos_7.7.conf @@ -0,0 +1,22 @@ +[main] +gpgcheck=0 +installonly_limit=3 +clean_requirements_on_remove=True +best=True +skip_if_unavailable=False +enabled=1 +cachedir=/tmp +reposdir=/dev/null + +[huaweicloud] +name = CentOS Linux 7.7 +baseurl = https://repo.huaweicloud.com/centos-vault/7.7.1908/os/x86_64/ + + +# [tuna] +# name = CentOS Linux 7.7 +# baseurl = https://mirrors.tuna.tsinghua.edu.cn/centos-vault/7.7.1908/os/x86_64/ + +# [debuginfo] +# name = debuginfo +# baseurl = http://debuginfo.centos.org/7/x86_64/ \ No newline at end of file diff --git a/src/conf/x86_64/centos_7.8.conf b/src/conf/x86_64/centos_7.8.conf new file mode 100644 index 0000000000000000000000000000000000000000..81d3912373a8fead129be34425645463a5addd08 --- /dev/null +++ b/src/conf/x86_64/centos_7.8.conf @@ -0,0 +1,22 @@ +[main] +gpgcheck=0 +installonly_limit=3 +clean_requirements_on_remove=True +best=True +skip_if_unavailable=False +enabled=1 +cachedir=/tmp +reposdir=/dev/null + +[huaweicloud] +name = CentOS Linux 7.8 +baseurl = https://repo.huaweicloud.com/centos-vault/7.8.2003/os/x86_64/ + + +#[tuna] +#name = CentOS Linux 7.8 +#baseurl = https://mirrors.tuna.tsinghua.edu.cn/centos-vault/7.8.2003/os/x86_64/ + +[debuginfo] +name = debuginfo +baseurl = http://debuginfo.centos.org/7/x86_64/ \ No newline at end of file diff --git a/src/conf/x86_64/centos_7.9.conf b/src/conf/x86_64/centos_7.9.conf new file mode 100644 index 0000000000000000000000000000000000000000..caca89e925f5dadafd3a28a4b1a4b707df377450 --- /dev/null +++ b/src/conf/x86_64/centos_7.9.conf @@ -0,0 +1,22 @@ +[main] +gpgcheck=0 +installonly_limit=3 +clean_requirements_on_remove=True +best=True +skip_if_unavailable=False +enabled=1 +cachedir=/tmp +reposdir=/dev/null + +[huaweicloud] +name = CentOS Linux 7.9 +baseurl = https://repo.huaweicloud.com/centos-vault/7.9.2009/os/x86_64/ + + +# [tuna] +# name = CentOS Linux 7.9 +# baseurl = https://mirrors.tuna.tsinghua.edu.cn/centos-vault/7.9.2009/os/x86_64/ + +# [debuginfo] +# name = debuginfo +# baseurl = http://debuginfo.centos.org/7/x86_64/ \ No newline at end of file diff --git a/src/logger.py b/src/logger.py new file mode 100644 index 0000000000000000000000000000000000000000..a837980496634f7c413ca76231205fa2eb81cfcb --- /dev/null +++ b/src/logger.py @@ -0,0 +1,147 @@ +""" +Customized logging functionality +CRITICAL (50) Calls critical() function and sys.exit(1) +ERROR (40) Prints error message using date/time +WARNING (30) Prints warning message using date/time +INFO (20) Prints info message (no date/time, just plain message) +TASK (15) CUSTOM LABEL - Prints a task header message (using asterisks) +DEBUG (10) Prints debug message (using date/time) +FILE (5) CUSTOM LABEL - Prints only to file handler (using date/time) +""" +import logging +import os +import sys + +from abicheck.toolopts import tool_opts +from abicheck.utils import format_msg_with_datetime + + +class LogLevelTask(object): + level = 15 + label = "TASK" + + +class LogLevelFile(object): + level = 5 + label = "FILE" + + +def initialize_logger(log_name, log_dir="/tmp"): + """Initialize custom logging levels, handlers, and so on. Call this method + from your application's main start point. + log_name = the name for the log file + log_dir = path to the dir where log file will be presented + """ + # set custom labels + logging.addLevelName(LogLevelTask.level, LogLevelTask.label) + logging.addLevelName(LogLevelFile.level, LogLevelFile.label) + logging.Logger.task = _task + logging.Logger.file = _file + logging.Logger.debug = _debug + logging.Logger.critical = _critical + + # enable raising exceptions + logging.raiseExceptions = True + # get root logger + logger = logging.getLogger("abicheck") + # propagate + logger.propagate = True + # set default logging level + logger.setLevel(LogLevelFile.level) + + # create sys.stdout handler for info/debug + stdout_handler = logging.StreamHandler(sys.stdout) + formatter = CustomFormatter("%(message)s") + formatter.disable_colors(tool_opts.disable_colors) + stdout_handler.setFormatter(formatter) + stdout_handler.setLevel(logging.DEBUG) + logger.addHandler(stdout_handler) + + # create file handler + if not os.path.isdir(log_dir): + os.makedirs(log_dir) # pragma: no cover + handler = logging.FileHandler(os.path.join(log_dir, log_name), "a") + formatter = CustomFormatter("%(message)s") + formatter.disable_colors(True) + handler.setFormatter(formatter) + handler.setLevel(LogLevelFile.level) + logger.addHandler(handler) + + +def _task(self, msg, *args, **kwargs): + if self.isEnabledFor(LogLevelTask.level): + self._log(LogLevelTask.level, msg, args, **kwargs) + + +def _file(self, msg, *args, **kwargs): + if self.isEnabledFor(LogLevelFile.level): + self._log(LogLevelFile.level, msg, args, **kwargs) + + +def _critical(self, msg, *args, **kwargs): + if self.isEnabledFor(logging.CRITICAL): + self._log(logging.CRITICAL, + format_msg_with_datetime(msg, "critical"), args, **kwargs) + sys.exit(1) + + +def _debug(self, msg, *args, **kwargs): + if self.isEnabledFor(logging.DEBUG): + from abicheck.toolopts import tool_opts + + if tool_opts.debug: + self._log(logging.DEBUG, msg, args, **kwargs) + else: + self._log(LogLevelFile.level, + format_msg_with_datetime(msg, "debug"), args, **kwargs) + + +class bcolors: + OKGREEN = "\033[92m" + WARNING = "\033[93m" + FAIL = "\033[91m" + ENDC = "\033[0m" + + +class CustomFormatter(logging.Formatter, object): + """Custom formatter to handle different logging formats based on logging level + Python 2.6 workaround - logging.Formatter class does not use new-style + class and causes 'TypeError: super() argument 1 must be type, not + classobj' so we use multiple inheritance to get around the problem. + """ + + color_disabled = False + + def disable_colors(self, value): + self.color_disabled = value + + def format(self, record): + if record.levelno == LogLevelTask.level: + temp = "*" * (90 - len(record.msg) - 25) + fmt_orig = "\n[%(asctime)s] %(levelname)s - [%(message)s] " + temp + new_fmt = fmt_orig if self.color_disabled else bcolors.OKGREEN + fmt_orig + bcolors.ENDC + self._fmt = new_fmt + self.datefmt = "%Y-%m-%d %H:%M:%S" + elif record.levelno in [logging.INFO, LogLevelFile.level]: + self._fmt = "%(message)s" + self.datefmt = "" + elif record.levelno in [logging.WARNING]: + fmt_orig = "%(levelname)s - %(message)s" + new_fmt = fmt_orig if self.color_disabled else bcolors.WARNING + fmt_orig + bcolors.ENDC + self._fmt = new_fmt + self.datefmt = "" + elif record.levelno in [logging.CRITICAL]: + fmt_orig = "%(levelname)s - %(message)s" + new_fmt = fmt_orig if self.color_disabled else bcolors.FAIL + fmt_orig + bcolors.ENDC + self._fmt = new_fmt + self.datefmt = "" + else: + self._fmt = "[%(asctime)s] %(levelname)s - %(message)s" + self.datefmt = "%Y-%m-%d %H:%M:%S" + + if hasattr(self, "_style"): + # Python 3 has _style for formatter + # Overwriting the style _fmt gets the result we want + self._style._fmt = self._fmt + + return super(CustomFormatter, self).format(record) diff --git a/src/main.py b/src/main.py new file mode 100644 index 0000000000000000000000000000000000000000..825e766b6abb8b12bf147074514a2e39289e20ec --- /dev/null +++ b/src/main.py @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- +# +# 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, see . + +import logging +import sys + +from abicheck import binhandler, toolopts, utils + +loggerinst = logging.getLogger("abicheck") + + +def main(): + """Perform all steps for the entire conversion process.""" + + utils.require_root() + + # handle command line arguments + cli = toolopts.CLI() + cli.process_cli_options() + + # check cmd + utils.check_cmd(binhandler.ABI_CC) + utils.check_cmd(binhandler.ABI_DUMPER) + utils.check_cmd(binhandler.READELF) + utils.check_cmd(binhandler.RPM2CPIO) + utils.check_cmd(binhandler.DOT) + utils.check_cmd(binhandler.CONVERT) + + checker = binhandler.ABI() + + checker.gen_elf_info() + checker.gen_ldd_info() + checker.gen_soname_file() + + # old + checker.get_old_os_main_pkgs() + checker.get_old_devel_pkgs() + checker.get_old_libs_pkgs() + checker.download_old_packages() + checker.decompress_old_packages() + checker.gen_old_xml() + checker.gen_old_dump() + + # new + checker.get_new_os_main_pkgs() + checker.get_new_devel_pkgs() + checker.get_new_libs_pkgs() + checker.download_new_packages() + checker.decompress_new_packages() + checker.gen_new_xml() + checker.gen_new_dump() + + # diff + checker.diff_dump() + + # library depdency + checker.gen_soname_deppng() + checker.gen_rpm_deppng() + + checker.add_deptab() + + # show result + checker.show_html() + + +if __name__ == "__main__": + + sys.exit(main()) diff --git a/src/toolopts.py b/src/toolopts.py index 70de9946196d6d45efbfbf919be562bc649f5be3..8e47c5733dffe7edf1ff5e58ef519b3ae67fb0a9 100644 --- a/src/toolopts.py +++ b/src/toolopts.py @@ -1,23 +1,188 @@ +# -*- coding: utf-8 -*- +# +# 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, see . + +import logging +import optparse import os +import platform import sys -import argparse - -def parse_args(): - desc = "Analyze abi infomation about bin file." - parser = argparse.ArgumentParser( - description=desc, epilog=f"example: {CMD_NAME} -tar /path/OLD-abi-info.tar.gz") - parser.add_argument('-v', action='version', - version='Package ABI Info Collector '+TOOL_VERSION) - parser.add_argument( - '-debug', help='enable debug messages', action='store_true') - parser.add_argument('-tar', metavar='OLD-abi-info.tar.gz', - help='abi info tarball file',) - parser.add_argument( - '-debuginfo', help=argparse.SUPPRESS, action='store_true') - parser.add_argument( - '-export-dir', help='specify a directory to save and reuse ABI info export (default: ./abi-info-export)', metavar='DIR') - #parser.add_argument('-src', help='collect source abi info', action='store_true') - return parser.parse_args() +import distro + +from abicheck import __version__, utils +from abicheck.utils import mkdir_p + +PROG = "abi-info-check" + +SUPPORT_OS = ( + "centos_7.1", + "centos_7.2", + "centos_7.3", + "centos_7.4", + "centos_7.5", + "centos_7.6", + "centos_7.7", + "centos_7.8", + "centos_7.9", +) + + +class ToolOpts(object): + def __init__(self): + self.prog = PROG + self.support_os = SUPPORT_OS + + self.debug = False + self.disable_colors = False + self.export_dir = "./abi-info-export" + self.binfile = "" + + self.old_os_full_name = None + # Old OS name (e.g. CentOS, UnionTech OS Server 20) + self.old_os_name = None + # Major verion of OS,(e.g. 7.1) + self.old_os_version = None + + self.old_dnf_conf = "" + + self.new_os_id = distro.id() + self.new_os_name = distro.name() + self.new_os_version = distro.major_version() + + +class CLI(object): + def __init__(self): + self._parser = self._get_argparser() + self._register_options() + + @staticmethod + def _get_argparser(): + usage = ( + "\n" + f" {PROG} [-h]\n" + f" {PROG} [--version]\n" + f" {PROG} [--bin BINFILE] [--os OS_VERSION]" + " [--export-dir DIR] [--debug] \n" + "\n\n" + "WARNING: The pre-migration operating system supported by the tool is" + f" {SUPPORT_OS}" + "\n" + "WARNING: The tool needs to be run under the root user") + return optparse.OptionParser( + conflict_handler="resolve", + usage=usage, + add_help_option=False, + version=__version__, + ) + + def _register_options(self): + """Prescribe what command line options the tool accepts.""" + self._parser.add_option( + "-h", + "--help", + action="help", + help="Show help message and exit.", + ) + self._parser.add_option( + "-v", + "--version", + action="version", + help=f"Show {PROG} version and exit.", + ) + + self._parser.add_option( + "--bin", + metavar="BINFILE", + help="Input binary file to be migrated.", + ) + self._parser.add_option( + "--os", + metavar="OS_VERSION", + choices=SUPPORT_OS, + help="Operating systems that support migration." + f" supported OS.VERSION is {SUPPORT_OS}", + ) + self._parser.add_option( + "--export-dir", + metavar="DIR", + help="Directory to save output file (default: ./abi-info-export)", + ) + + self._parser.add_option( + "--debug", + action="store_true", + help= + "Print traceback in case of an abnormal exit and messages that could help find an issue.", + ) + self._parser.add_option( + "--disable-colors", + action="store_true", + help=optparse.SUPPRESS_HELP, + ) + + def process_cli_options(self): + """Process command line options used with the tool.""" + warn_on_unsupported_options() + + parsed_opts, _ = self._parser.parse_args() + + global tool_opts # pylint: disable=C0103 + + if parsed_opts.export_dir: + tool_opts.export_dir = parsed_opts.export_dir + if not os.path.exists(tool_opts.export_dir): + mkdir_p(tool_opts.export_dir) + from abicheck import logger + logger.initialize_logger("abicheck.log", tool_opts.export_dir) + loggerinst = logging.getLogger(__name__) + loggerinst.info(f"The export-dir is {tool_opts.export_dir}.") + + if parsed_opts.debug: + tool_opts.debug = True + + if parsed_opts.disable_colors: + tool_opts.disable_colors = True + + if parsed_opts.os: + tool_opts.old_os_full_name = parsed_opts.os + tool_opts.old_os_name = tool_opts.old_os_full_name.split('_')[0] + tool_opts.old_os_version = tool_opts.old_os_full_name.split('_')[1] + arch = platform.machine() + tool_opts.old_dnf_conf = f'{utils.DATA_DIR}/conf/{arch}/{tool_opts.old_os_full_name}.conf' + else: + loggerinst.critical( + "Error: --os is required.") + + if parsed_opts.bin: + tool_opts.binfile = parsed_opts.bin + if not utils.isbinary(tool_opts.binfile): + loggerinst.critical( + f"Error: {tool_opts.binfile} isn't a binary file.") + + else: + loggerinst.critical( + "Error: --bin is required.") + pass + + +def warn_on_unsupported_options(): + loggerinst = logging.getLogger(__name__) + if any(x in sys.argv[1:] for x in ["--debuginfo"]): + loggerinst.critical("The --debuginfo option is not supported.\n" + f"See {PROG} -h for more information.") +# Code to be executed upon module import +tool_opts = ToolOpts() # pylint: disable=C0103 diff --git a/src/utils.py b/src/utils.py index 2ac5bfec09cccb2b328c80bfcaff0dd9d5f9d4af..9c592a89c2e5a52a64156ab9affb99fe099ce6dd 100644 --- a/src/utils.py +++ b/src/utils.py @@ -13,199 +13,359 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import datetime +import errno +import getpass +import inspect import logging import os -import re +import shlex import stat import subprocess import sys +import traceback -import xml.etree.ElementTree as ET -import pandas as pd +import dnf +import pexpect +from six import moves -def run_cmd(cmd): + +def check_cmd(prog): + loggerinst = logging.getLogger(__name__) + """Return prog of absolute if prog is in $PATH .""" + for path in os.environ['PATH'].split(os.pathsep): + path = path.strip('"') + candidate = os.path.join(path, prog) + if os.path.isfile(candidate) and os.access(candidate, os.X_OK): + return candidate + loggerinst.critical(f"{prog} executable command not found.") + + +def isbinary(filepath): + """Return true if filepath is a standard binary file""" + loggerinst = logging.getLogger(__name__) + if not os.path.exists(filepath): + loggerinst.info('file path {} doesnot exits'.format(filepath)) + return False + # 文件可能被损坏,捕捉异常 + try: + FileStates = os.stat(filepath) + FileMode = FileStates[stat.ST_MODE] + if not stat.S_ISREG(FileMode) or stat.S_ISLNK( + FileMode): # 如果文件既不是普通文件也不是链接文件 + return False + with open(filepath, 'rb') as f: + header = (bytearray(f.read(4))[1:4]).decode(encoding="utf-8") + if header in ["ELF"]: + # print (header) + return True + except UnicodeDecodeError as e: + pass + return False + + +def dnf_list(config, arch): + """List the package name from the config""" + loggerinst = logging.getLogger(__name__) + base = dnf.Base() + + if os.path.exists(config): + conf = base.conf + conf.read(config) + else: + loggerinst.critical(f"No such file {config}, please check.") + + base.read_all_repos() + try: + base.fill_sack() + except (dnf.exceptions.RepoError, dnf.exceptions.ConfigError) as e: + loggerinst.critical(e) + query = base.sack.query() + a = query.available() + pkgs = a.filter(arch=arch) + pkgs_list = list() + for pkg in pkgs: + pkgs_list.append(pkg.name) + if pkgs_list: + return pkgs_list + return [] + + +def dnf_provides(config, substr, arch): + loggerinst = logging.getLogger(__name__) + loggerinst.debug(f"Querying which package provides {substr}.") + base = dnf.Base() + + if os.path.exists(config): + conf = base.conf + conf.read(config) + else: + loggerinst.critical(f"No such file {config}, please check.") + + base.read_all_repos() + try: + base.fill_sack() + except (dnf.exceptions.RepoError, dnf.exceptions.ConfigError) as e: + loggerinst.critical(e) + query = base.sack.query() + a = query.available() + pkgs = a.filter(file__glob=substr, arch=arch,) + pkgs_list=list() + if pkgs: + for pkg in pkgs: + pkgs_list.append(pkg.name) + loggerinst.debug( + f"{substr} is provides by {pkgs_list}") + else: + pkgs_list=[] + loggerinst.debug( + f"Not found package provide {substr}") + return pkgs_list + + +class Color(object): + PURPLE = '\033[95m' + CYAN = '\033[96m' + DARKCYAN = '\033[36m' + BLUE = '\033[94m' + GREEN = '\033[92m' + YELLOW = '\033[93m' + RED = '\033[91m' + BOLD = '\033[1m' + UNDERLINE = '\033[4m' + END = '\033[0m' + + +# Absolute path of a directory holding data for this tool +DATA_DIR = "/usr/share/abicheck" +# Directory for temporary data to be stored during runtime +TMP_DIR = "/var/lib/abicheck" + + +def format_msg_with_datetime(msg, level): + """Return a string with msg formatted according to the level""" + temp_date = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + return f"[{temp_date}] {level.upper()} - {msg}" + + +def get_executable_name(): + """Get name of the executable file passed to the python interpreter.""" + return os.path.basename(inspect.stack()[-1][1]) + + +def require_root(): + if os.geteuid() != 0: + print("The tool needs to be run under the root user.") + print("\nNo changes were made to the system.") + sys.exit(1) + + +def get_file_content(filename, as_list=False): + """Return content of a file either as a list of lines or as a multiline + string. + """ + lines = [] + if not os.path.exists(filename): + if not as_list: + return "" + return lines + file_to_read = open(filename, "r") + try: + lines = file_to_read.readlines() + finally: + file_to_read.close() + if as_list: + # remove newline character from each line + return [x.strip() for x in lines] + + return "".join(lines) + + +def store_content_to_file(filename, content): + """Write the content into the file. + + Accept string or list of strings (in that case every string will be written + on separate line). In case the ending blankline is missing, the newline + character is automatically appended to the file. + """ + if isinstance(content, list): + content = "\n".join(content) + if len(content) > 0 and content[-1] != "\n": + # append the missing newline to comply with standard about text files + content += "\n" + file_to_write = open(filename, "w") + try: + file_to_write.write(content) + finally: + file_to_write.close() + + +def run_cmd(cmd, print_cmd=True): ''' run command in subprocess and return exit code, output, error. ''' + loggerinst = logging.getLogger(__name__) + if print_cmd: + loggerinst.debug("Calling command '%s'" % cmd) + loggerinst.file("Calling command '%s'" % cmd) + os.putenv('LANG', 'C') os.putenv('LC_ALL', 'C') os.environ['LANG'] = 'C' os.environ['LC_ALL'] = 'C' cmd = cmd.encode('UTF-8') - proc = subprocess.Popen(cmd, shell=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) + proc = subprocess.Popen( + cmd, + shell=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) (stdout, stderr) = proc.communicate() returncode = proc.returncode return (returncode, stdout, stderr) -def find_devel_lib_package(export_dir): - print('Checking the dependency ...') - file = f'{export_dir}/OLD-pkgs.info' - if not os.access(f'{file}', os.R_OK): - exit_status( - "Error", f"The info file {file} can't be read. Please check") - - dev_pkgs_set = set() - libs_pkgs_set = set() - dbginfo_pkgs_set = set() - with open(file, 'r') as f: - for line in f: - rpm_name = get_rpmname_without_libs(line) - dev_pkgs_set.add(f'{rpm_name}-devel') - dbginfo_pkgs_set.add(f'{rpm_name}-debuginfo') - # 包名中包含 lib 关键字的包,本身就是 lib 包 - if 'lib' not in rpm_name: - libs_pkgs_set.add(f'{rpm_name}-libs') - else: - libs_pkgs_set.add(f'{rpm_name}') - return dev_pkgs_set, libs_pkgs_set, dbginfo_pkgs_set - -def try_install_packages(dev_pkgs, libs_pkgs=set()): - not_installed_pkgs = set() - - dev_pkgs = set(dev_pkgs) | set(libs_pkgs) - for pkg in dev_pkgs: - cmd = f'rpm -q {pkg}' - returncode, stdout, stderr = run_cmd(cmd) - if returncode != 0: - print(f"It seems that the OS doesn't install {pkg}") - not_installed_pkgs.add(pkg) + +def run_subprocess(cmd="", print_cmd=True, print_output=True): + """Call the passed command and optionally log the called command (print_cmd=True) and its + output (print_output=True). Switching off printing the command can be useful in case it contains + a password in plain text. + """ + loggerinst = logging.getLogger(__name__) + if print_cmd: + loggerinst.debug("Calling command '%s'" % cmd) + loggerinst.file("Calling command '%s'" % cmd) + + # Python 2.6 has a bug in shlex that interprets certain characters in a string as + # a NULL character. This is a workaround that encodes the string to avoid the issue. + if sys.version_info[0] == 2 and sys.version_info[1] == 6: + cmd = cmd.encode("ascii") + cmd = shlex.split(cmd, False) + process = subprocess.Popen(cmd, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + bufsize=1, + env={'LC_ALL': 'C'}) + output = '' + for line in iter(process.stdout.readline, b''): + output += line.decode() + if print_output: + loggerinst.info(line.decode().rstrip('\n')) + + # Call communicate() to wait for the process to terminate so that we can get the return code by poll(). + # It's just for py2.6, py2.7+/3 doesn't need this. + process.communicate() + + return_code = process.poll() + return output, return_code + + +def run_cmd_in_pty(cmd="", print_cmd=True, print_output=True, columns=120): + """Similar to run_subprocess(), but the command is executed in a pseudo-terminal. + + The pseudo-terminal can be useful when a command prints out a different output with or without an active terminal + session. E.g. yumdownloader does not print the name of the downloaded rpm if not executed from a terminal. + Switching off printing the command can be useful in case it contains a password in plain text. + + :param cmd: The command to execute, including the options, e.g. "ls -al" + :type cmd: string + :param print_cmd: Log the command (to both logfile and stdout) + :type print_cmd: bool + :param print_output: Log the combined stdout and stderr of the executed command (to both logfile and stdout) + :type print_output: bool + :param columns: Number of columns of the pseudo-terminal (characters on a line). This may influence the output. + :type columns: int + :return: The output (combined stdout and stderr) and the return code of the executed command + :rtype: tuple + """ + loggerinst = logging.getLogger(__name__) + + process = pexpect.spawn(cmd, env={'LC_ALL': 'C'}, timeout=None) + if print_cmd: + # This debug print somehow needs to be called after(!) the pexpect.spawn(), otherwise the setwinsize() + # wouldn't have any effect. Weird. + loggerinst.debug("Calling command '%s'" % cmd) + + process.setwinsize(0, columns) + process.expect(pexpect.EOF) + output = process.before.decode() + if print_output: + loggerinst.info(output.rstrip('\n')) + + process.close( + ) # Per the pexpect API, this is necessary in order to get the return code + return_code = process.exitstatus + + return output, return_code + + +def let_user_choose_item(num_of_options, item_to_choose): + """Ask user to enter a number corresponding to the item they choose.""" + loggerinst = logging.getLogger(__name__) + while True: # Loop until user enters a valid number + opt_num = prompt_user("Enter number of the chosen %s: " % + item_to_choose) + try: + opt_num = int(opt_num) + except ValueError: + loggerinst.warning("Enter a valid number.") + # Ensure the entered number is in the proper range + if 0 < opt_num <= num_of_options: + break else: - print(f'The packages "{pkg}" have been installed') - - install_failed_pkgs = set() - for pkg in not_installed_pkgs: - cmd = f'yum install -y {pkg}' - print(f'Trying to install package {pkg} with yum') - returncode, stdout, stderr = run_cmd(cmd) - if returncode != 0: - print(f"Can't install {pkg}, with yum") - install_failed_pkgs.add(pkg) + loggerinst.warning("The entered number is not in range" + " 1 - %s." % num_of_options) + return opt_num - 1 # Get zero-based list index + + +def mkdir_p(path): + """Create all missing directories for the path and raise no exception + if the path exists. + """ + try: + os.makedirs(path) + except OSError as err: + if err.errno == errno.EEXIST and os.path.isdir(path): + pass else: - print(f'Successfully installed {pkg} with yum') - - if install_failed_pkgs: - exit_status( - "Error", f'Please install {install_failed_pkgs}, then retry') - - -def gen_xml_and_dump(abi_name, target_path, dev_pkgs_set, libs_pkgs_set): - headers_list = list() - for pkg in dev_pkgs_set: - cmd = f'rpm -ql {pkg} | grep .*include.*\.h$' - returncode, stdout, stderr = run_cmd(cmd) - headers_list.append(stdout.decode()) - - libs_list = list() - for pkg in libs_pkgs_set: - cmd = f'rpm -ql {pkg} | grep .*lib.*\.so\.*$' - returncode, stdout, stderr = run_cmd(cmd) - libs_list.append(stdout.decode()) - - file = f'{target_path}/NEW-dump.xml' - with open(file, 'wt+') as f: - f.write("\n") - f.write('1.0\n') - f.write('\n') - - f.write("\n") - f.writelines(headers_list) - f.write("\n") - - f.write("\n") - f.writelines(libs_list) - f.write("\n") - dump_file = f'{target_path}/NEW-abi.dump' - cmd = f'abi-compliance-checker -xml -l {abi_name} -dump {file} -dump-path {dump_file}' - print(f'Analyzing the symbols of {libs_pkgs_set} ...') - run_cmd(cmd) - - return dump_file - -def compare_syms(export_dir, dump_file): - print('Checking symbol differences ...') - elf_sym_set = set() - elf_file = f'{export_dir}/OLD-elf.info' - with open(f'{elf_file}', 'rt') as f: - elf_symbol_fmt = ' *(?P[0-9]*): (?P[0-9abcdef]*) (?P[0-9]*).*(FUNC).*@.*' - for line in f: - m = re.match(elf_symbol_fmt, line) - if not m: - continue - elf_line_list = re.split(r'\s+', line.strip()) - sym = elf_line_list[7].split('@')[0] - - elf_sym_set.add(sym) - - library_sym_list = set() - - with open(f'{dump_file}', 'r') as file: - context = file.read().replace('& ', '') - - with open(f'{dump_file}', 'w+') as file: - file.writelines(context) - - tree = ET.parse(f'{dump_file}') - for line in tree.iterfind('symbols/library/symbol'): - sym = line.text.split('@@')[0] - library_sym_list.add(sym) - diff_syms_list = list((elf_sym_set - library_sym_list)) - old_syms_list = list(elf_sym_set) - - return [diff_syms_list, old_syms_list] - - -def check_cmd(prog): - for path in os.environ['PATH'].split(os.pathsep): - path = path.strip('"') - candidate = path+"/"+prog - if os.path.isfile(candidate) and os.access(candidate, os.X_OK): - return candidate - return None - - -def get_abi_name(export_dir): - file = f'{export_dir}/OLD-name.info' - if not os.access(f'{file}', os.R_OK): - exit_status( - "Error", f"The info file {file} can't be read, Please check") - with open(file, 'r') as f: - name = f.read() - return name - - -def check_dump_syms(abi_name, export_dir): - new_dump = f'{export_dir}/NEW-abi.dump' - old_dump = f'{export_dir}/OLD-abi.dump' - syms_list = f'{export_dir}/OLD-func-syms.info' - html = f'{export_dir}/export.html' - cmd = f'abi-compliance-checker -l {abi_name} -old {old_dump} -new {new_dump} --symbols-list {syms_list} --report-path {html}' - print(f'Checking the symbols of {abi_name} ...') - run_cmd(cmd) - return html - - -def output_result(syms_list, export_dir): - df1 = pd.DataFrame(syms_list[0], columns=[u'当前系统缺少符号']) - df2 = pd.DataFrame(syms_list[1], columns=[u'二进制依赖符号']) - - result = pd.concat([df1, df2], axis=1) - # 用空替换表格中的 NaN - result = result.fillna('') - - html = result.to_html() - csv = result.to_csv() - - file = f'{export_dir}/result.html' - with open(file, 'wt+') as f: - f.writelines(html) - html_path = os.path.realpath(file) - - file = f'{export_dir}/result.csv' - with open(file, 'wt+') as f: - f.writelines(csv) - - print(f'The check result is {html_path}') - + raise + + +def prompt_user(question, password=False): + loggerinst = logging.getLogger(__name__) + color_question = Color.BOLD + question + Color.END + if password: + response = getpass.getpass(color_question) + else: + response = moves.input(color_question) + loggerinst.info("\n") + return response + + +def log_traceback(debug): + """Log a traceback either to both a file and stdout, or just file, based + on the debug parameter. + """ + loggerinst = logging.getLogger(__name__) + traceback_str = get_traceback_str() + if debug: + # Print the traceback to the user when debug option used + loggerinst.debug(traceback_str) + else: + # Print the traceback to the log file in any way + loggerinst.file(traceback_str) + + +def get_traceback_str(): + """Get a traceback of an exception as a string.""" + exc_type, exc_value, exc_traceback = sys.exc_info() + return "".join( + traceback.format_exception(exc_type, exc_value, exc_traceback)) + + +class DictWListValues(dict): + """Python 2.4 replacement for Python 2.5+ collections.defaultdict(list).""" + def __getitem__(self, item): + if item not in iter(self.keys()): + self[item] = [] + + return super(DictWListValues, self).__getitem__(item)