| #!/usr/bin/env python |
| # -*- coding: utf-8 -*- |
| # -*- Mode: Python |
| # |
| # Copyright (C) 2013-2016 Red Hat, Inc. |
| # |
| # This file is part of the GNU Application Binary Interface Generic |
| # Analysis and Instrumentation Library (libabigail). This library 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, or (at your option) any |
| # later version. |
| # |
| # This library 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; see the file COPYING-GPLV3. If |
| # not, see <http:#www.gnu.org/licenses/>. |
| # |
| # Author: Chenxiong Qi |
| |
| from __future__ import print_function |
| |
| import argparse |
| import functools |
| import glob |
| import logging |
| import mimetypes |
| import os |
| import re |
| import shutil |
| import six |
| import subprocess |
| import sys |
| |
| from collections import namedtuple |
| from itertools import chain |
| |
| import xdg.BaseDirectory |
| |
| import rpm |
| import koji |
| |
| # @file |
| # |
| # You might have known that abipkgdiff is a command line tool to compare two |
| # RPM packages to find potential differences of ABI. This is really useful for |
| # Fedora packagers and developers. Usually, excpet the RPM packages built |
| # locally, if a packager wants to compare RPM packages he just built with |
| # specific RPM packages that were already built and availabe in Koji, |
| # fedabipkgdiff is the right tool for him. |
| # |
| # With fedabipkgdiff, packager is able to specify certain criteria to tell |
| # fedabipkgdiff which RPM packages he wants to compare, then fedabipkgdiff will |
| # find them, download them, and boom, run the abipkgdiff for you. |
| # |
| # Currently, fedabipkgdiff returns 0 if everything works well, otherwise, 1 if |
| # something wrong. |
| |
| |
| koji_config = koji.read_config('koji') |
| DEFAULT_KOJI_SERVER = koji_config['server'] |
| DEFAULT_KOJI_TOPURL = koji_config['topurl'] |
| |
| # The working directory where to hold all data including downloaded RPM |
| # packages Currently, it's not configurable and hardcode here. In the future |
| # version of fedabipkgdiff, I'll make it configurable by users. |
| HOME_DIR = os.path.join(xdg.BaseDirectory.xdg_cache_home, |
| os.path.splitext(os.path.basename(__file__))[0]) |
| |
| DEFAULT_ABIPKGDIFF = 'abipkgdiff' |
| |
| # Mask for determining if underlying fedabipkgdiff succeeds or not. |
| # This is for when the compared ABIs are equal |
| ABIDIFF_OK = 0 |
| # This bit is set if there an application error. |
| ABIDIFF_ERROR = 1 |
| # This bit is set if the tool is invoked in an non appropriate manner. |
| ABIDIFF_USAGE_ERROR = 1 << 1 |
| # This bit is set if the ABIs being compared are different. |
| ABIDIFF_ABI_CHANGE = 1 << 2 |
| |
| |
| # Used to construct abipkgdiff command line argument, package and associated |
| # debuginfo package |
| # fedabipkgdiff runs abipkgdiff in this form |
| # |
| # abipkgdiff \ |
| # --d1 /path/to/package1-debuginfo.rpm \ |
| # --d2 /path/to/package2-debuginfo.rpm \ |
| # /path/to/package1.rpm \ |
| # /path/to/package2.rpm |
| # |
| # ComparisonHalf is a three-elements tuple in format |
| # |
| # (package1.rpm, [package1-debuginfo.rpm..] package1-devel.rpm) |
| # |
| # - the first element is the subject representing the package to |
| # compare. It's a dict representing the RPM we are interested in. |
| # That dict was retrieved from Koji XMLRPC API. |
| # - the rest are ancillary packages used for the comparison. So, the |
| # second one is a vector containing the needed debuginfo packages |
| # (yes there can be more than one), and the last one is the package |
| # containing API of the ELF shared libraries carried by subject. |
| # All the packages are dicts representing RPMs and those dicts were |
| # retrieved fromt he KOji XMLRPC API. |
| # |
| # So, before calling abipkgdiff, fedabipkgdiff must prepare and pass |
| # the following information |
| # |
| # (/path/to/package1.rpm, [/paths/to/package1-debuginfo.rpm ..] /path/to/package1-devel.rpm) |
| # (/path/to/package2.rpm, [/paths/to/package2-debuginfo.rpm ..] /path/to/package1-devel.rpm) |
| # |
| ComparisonHalf = namedtuple('ComparisonHalf', |
| ['subject', 'ancillary_debug', 'ancillary_devel']) |
| |
| |
| global_config = None |
| pathinfo = None |
| session = None |
| |
| # There is no way to configure the log format so far. I hope I would have time |
| # to make it available so that if fedabipkgdiff is scheduled and run by some |
| # service, the logs logged into log file is muc usable. |
| logging.basicConfig(format='[%(levelname)s] %(message)s', level=logging.CRITICAL) |
| logger = logging.getLogger(os.path.basename(__file__)) |
| |
| |
| class KojiPackageNotFound(Exception): |
| """Package is not found in Koji""" |
| |
| |
| class PackageNotFound(Exception): |
| """Package is not found locally""" |
| |
| |
| class RpmNotFound(Exception): |
| """RPM is not found""" |
| |
| |
| class NoBuildsError(Exception): |
| """No builds returned from a method to select specific builds""" |
| |
| |
| class NoCompleteBuilds(Exception): |
| """No complete builds for a package |
| |
| This is a serious problem, nothing can be done if there is no complete |
| builds for a package. |
| """ |
| |
| |
| class InvalidDistroError(Exception): |
| """Invalid distro error""" |
| |
| |
| class CannotFindLatestBuildError(Exception): |
| """Cannot find latest build from a package""" |
| |
| |
| class SetCleanCacheAction(argparse._StoreTrueAction): |
| """Custom Action making clean-cache as bundle of clean-cache-before and clean-cache-after""" |
| |
| def __call__(self, parser, namespace, values, option_string=None): |
| setattr(namespace, 'clean_cache_before', self.const) |
| setattr(namespace, 'clean_cache_after', self.const) |
| |
| |
| def is_distro_valid(distro): |
| """Adjust if a distro is valid |
| |
| Currently, check for Fedora and RHEL. |
| |
| :param str distro: a string representing a distro value. |
| :return: True if distro is the one specific to Fedora, like fc24, el7. |
| "rtype: bool |
| """ |
| return re.match(r'^(fc|el)\d{1,2}$', distro) is not None |
| |
| |
| def get_distro_from_string(str): |
| """Get the part of a string that designates the Fedora distro version number |
| |
| For instance, when passed the string '2.3.fc12', this function |
| returns the string 'fc12'. |
| |
| :param str the string to consider |
| :return: The sub-string of the parameter that represents the |
| Fedora distro version number, or None if the parameter does not |
| contain such a sub-string. |
| """ |
| |
| m = re.match(r'(.*)((fc|el)\d{1,2})(.*)', str) |
| if not m: |
| return None |
| |
| distro = m.group(2) |
| return distro |
| |
| |
| def match_nvr(s): |
| """Determine if a string is a N-V-R""" |
| return re.match(r'^([^/]+)-(.+)-(.+)$', s) is not None |
| |
| |
| def match_nvra(s): |
| """Determine if a string is a N-V-R.A""" |
| return re.match(r'^([^/]+)-(.+)-(.+)\.(.+)$', s) is not None |
| |
| |
| def is_rpm_file(filename): |
| """Return if a file is a RPM""" |
| return os.path.isfile(filename) and \ |
| mimetypes.guess_type(filename)[0] == 'application/x-rpm' |
| |
| |
| def cmp_nvr(left, right): |
| """Compare function for sorting a sequence of NVRs |
| |
| This is the compare function used in sorted function to sort builds so that |
| fedabipkgdiff is able to select the latest build. Return value follows the |
| rules described in the part of paramter cmp of sorted documentation. |
| |
| :param str left: left nvr to compare. |
| :param str right: right nvr to compare. |
| :return: -1, 0, or 1 that represents left is considered smaller than, |
| equal to, or larger than the right individually. |
| :rtype: int |
| """ |
| left_nvr = koji.parse_NVR(left['nvr']) |
| right_nvr = koji.parse_NVR(right['nvr']) |
| return rpm.labelCompare( |
| (left_nvr['epoch'], left_nvr['version'], left_nvr['release']), |
| (right_nvr['epoch'], right_nvr['version'], right_nvr['release'])) |
| |
| |
| def log_call(func): |
| """A decorator that logs a method invocation |
| |
| Method's name and all arguments, either positional or keyword arguments, |
| will be logged by logger.debug. Also, return value from the decorated |
| method will be logged just after the invocation is done. |
| |
| This decorator does not catch any exception thrown from the decorated |
| method. If there is any exception thrown from decorated method, you can |
| catch them in the caller and obviously, no return value is logged. |
| |
| :param callable func: a callable object to decorate |
| """ |
| def proxy(*args, **kwargs): |
| logger.debug('Call %s, args: %s, kwargs: %s', |
| func.__name__, |
| args if args else '', |
| kwargs if kwargs else '') |
| result = func(*args, **kwargs) |
| logger.debug('Result from %s: %s', func.__name__, result) |
| return result |
| return proxy |
| |
| |
| def delete_download_cache(): |
| """Delete download cache directory""" |
| download_dir = get_download_dir() |
| if global_config.dry_run: |
| print('DRY-RUN: Delete cached downloaded RPM packages at {0}'.format(download_dir)) |
| else: |
| logger.debug('Delete cached downloaded RPM packages at {0}'.format(download_dir)) |
| shutil.rmtree(download_dir) |
| |
| |
| class RPM(object): |
| """Wrapper around an RPM descriptor received from Koji |
| |
| The RPM descriptor that is returned from Koji XMLRPC API is a |
| dict. This wrapper class makes it eaiser to access all these |
| properties in the way of object.property. |
| """ |
| |
| def __init__(self, rpm_info): |
| """Initialize a RPM object |
| |
| :param dict rpm_info: a dict representing an RPM descriptor |
| received from the Koji API, either listRPMs or getRPM |
| """ |
| self.rpm_info = rpm_info |
| |
| def __str__(self): |
| """Return the string representation of this RPM |
| |
| Return the string representation of RPM information returned from Koji |
| directly so that RPM can be treated in same way. |
| """ |
| return str(self.rpm_info) |
| |
| def __getattr__(self, name): |
| """Access RPM information in the way of object.property |
| |
| :param str name: the property name to access. |
| :raises AttributeError: if name is not one of keys of RPM information. |
| """ |
| if name in self.rpm_info: |
| return self.rpm_info[name] |
| else: |
| raise AttributeError('No attribute name {0}'.format(name)) |
| |
| def is_peer(self, another_rpm): |
| """Determine if this is the peer of a given rpm. |
| |
| Here is what "peer" means. |
| |
| Consider a package P for which the tripplet Name, Version, |
| Release is made of the values {N,V,R}. Then, consider a |
| package P' for which the similar tripplet is {N', V', R'}. |
| |
| P' is a peer of P if N == N', and either V != V' or R != R'. |
| given package with a given NVR is another package with a N'V' |
| """ |
| return self.name == another_rpm.name and \ |
| self.arch == another_rpm.arch and \ |
| not (self.version == another_rpm.version |
| and self.release == another_rpm.release) |
| |
| @property |
| def nvra(self): |
| """Return a RPM's N-V-R-A representation |
| |
| An example: libabigail-1.0-0.8.rc4.1.fc23.x86_64 |
| """ |
| nvra, _ = os.path.splitext(self.filename) |
| return nvra |
| |
| @property |
| def filename(self): |
| """Return a RPM file name |
| |
| An example: libabigail-1.0-0.8.rc4.1.fc23.x86_64.rpm |
| """ |
| return os.path.basename(pathinfo.rpm(self.rpm_info)) |
| |
| @property |
| def is_debuginfo(self): |
| """Check if the name of the current RPM denotes a debug info package""" |
| return koji.is_debuginfo(self.rpm_info['name']) |
| |
| @property |
| def is_devel(self): |
| """Check if the name of current RPM denotes a development package""" |
| return self.rpm_info['name'].endswith('-devel') |
| |
| @property |
| def download_url(self): |
| """Get the URL from where to download this RPM""" |
| build = session.getBuild(self.build_id) |
| return os.path.join(pathinfo.build(build), pathinfo.rpm(self.rpm_info)) |
| |
| @property |
| def downloaded_file(self): |
| """Get a pridictable downloaded file name with absolute path""" |
| # arch should be removed from the result returned from PathInfo.rpm |
| filename = os.path.basename(pathinfo.rpm(self.rpm_info)) |
| return os.path.join(get_download_dir(), filename) |
| |
| @property |
| def is_downloaded(self): |
| """Check if this RPM was already downloaded to local disk""" |
| return os.path.exists(self.downloaded_file) |
| |
| |
| class LocalRPM(RPM): |
| """Representing a local RPM |
| |
| Local RPM means the one that could be already downloaded or built from |
| where I can find it |
| """ |
| |
| def __init__(self, filename): |
| """Initialize local RPM with a filename |
| |
| :param str filename: a filename pointing to a RPM file in local |
| disk. Note that, this file must not exist necessarily. |
| """ |
| self.local_filename = filename |
| self.rpm_info = koji.parse_NVRA(os.path.basename(filename)) |
| |
| @property |
| def downloaded_file(self): |
| """Return filename of this RPM |
| |
| Returned filename is just the one passed when initializing this RPM. |
| |
| :return: filename of this RPM |
| :rtype: str |
| """ |
| return self.local_filename |
| |
| @property |
| def download_url(self): |
| raise NotImplementedError('LocalRPM has no URL to download') |
| |
| def _find_rpm(self, rpm_filename): |
| """Search an RPM from the directory of the current instance of LocalRPM |
| |
| :param str rpm_filename: filename of rpm to find, for example |
| foo-devel-0.1-1.fc24. |
| :return: an instance of LocalRPM representing the found rpm, or None if |
| no RPM was found. |
| """ |
| search_dir = os.path.dirname(os.path.abspath(self.local_filename)) |
| filename = os.path.join(search_dir, rpm_filename) |
| return LocalRPM(filename) if os.path.exists(filename) else None |
| |
| @log_call |
| def find_debuginfo(self): |
| """Find debuginfo RPM package from a directory""" |
| filename = \ |
| '%(name)s-debuginfo-%(version)s-%(release)s.%(arch)s.rpm' % \ |
| self.rpm_info |
| return self._find_rpm(filename) |
| |
| @log_call |
| def find_devel(self): |
| """Find development package from a directory""" |
| filename = \ |
| '%(name)s-devel-%(version)s-%(release)s.%(arch)s.rpm' % \ |
| self.rpm_info |
| return self._find_rpm(filename) |
| |
| |
| class RPMCollection(object): |
| """Collection of RPMs |
| |
| This is a simple collection containing RPMs collected from a |
| directory on the local filesystem or retrieved from Koji. |
| |
| A collection can contain one or more sets of RPMs. Each set of |
| RPMs being for a particular architecture. |
| |
| For a given architecture, a set of RPMs is made of one RPM and its |
| ancillary RPMs. An ancillary RPM is either a debuginfo RPM or a |
| devel RPM. |
| |
| So a given RPMCollection would (informally) look like: |
| |
| { |
| i686 => {foo.i686.rpm, foo-debuginfo.i686.rpm, foo-devel.i686.rpm} |
| x86_64 => {foo.x86_64.rpm, foo-debuginfo.x86_64.rpm, foo-devel.x86_64.rpm,} |
| } |
| |
| """ |
| |
| def __init__(self, rpms=None): |
| # Mapping from arch to a list of rpm_infos. |
| # Note that *all* RPMs of the collections are present in this |
| # map; that is the RPM to consider and its ancillary RPMs. |
| self.rpms = {} |
| |
| # Mapping from arch to another mapping containing index of debuginfo |
| # and development package |
| # e.g. |
| # self.ancillary_rpms = {'i686', {'debuginfo': foo-debuginfo.rpm, |
| # 'devel': foo-devel.rpm}} |
| self.ancillary_rpms = {} |
| |
| if rpms: |
| for rpm in rpms: |
| self.add(rpm) |
| |
| @classmethod |
| def gather_from_dir(cls, rpm_file, all_rpms=None): |
| """Gather RPM collection from local directory""" |
| dir_name = os.path.dirname(os.path.abspath(rpm_file)) |
| filename = os.path.basename(rpm_file) |
| |
| nvra = koji.parse_NVRA(filename) |
| rpm_files = glob.glob(os.path.join( |
| dir_name, '*-%(version)s-%(release)s.%(arch)s.rpm' % nvra)) |
| rpm_col = cls() |
| |
| if all_rpms: |
| selector = lambda rpm: True |
| else: |
| selector = lambda rpm: local_rpm.is_devel or \ |
| local_rpm.is_debuginfo or local_rpm.filename == filename |
| |
| found_debuginfo = 1 |
| |
| for rpm_file in rpm_files: |
| local_rpm = LocalRPM(rpm_file) |
| |
| if local_rpm.is_debuginfo: |
| found_debuginfo <<= 1 |
| if found_debuginfo == 4: |
| raise RuntimeError( |
| 'Found more than one debuginfo package in ' |
| 'this directory. At the moment, fedabipkgdiff ' |
| 'is not able to deal with this case. ' |
| 'Please create two separate directories and ' |
| 'put an RPM and its ancillary debuginfo and ' |
| 'devel RPMs in each directory.') |
| |
| if selector(local_rpm): |
| rpm_col.add(local_rpm) |
| |
| return rpm_col |
| |
| def add(self, rpm): |
| """Add a RPM into this collection""" |
| self.rpms.setdefault(rpm.arch, []).append(rpm) |
| |
| devel_debuginfo_default = {'debuginfo': None, 'devel': None} |
| |
| if rpm.is_debuginfo: |
| self.ancillary_rpms.setdefault( |
| rpm.arch, devel_debuginfo_default)['debuginfo'] = rpm |
| |
| if rpm.is_devel: |
| self.ancillary_rpms.setdefault( |
| rpm.arch, devel_debuginfo_default)['devel'] = rpm |
| |
| def rpms_iter(self, arches=None, default_behavior=True): |
| """Iterator of RPMs to go through RPMs with specific arches""" |
| arches = sorted(self.rpms.keys()) |
| |
| for arch in arches: |
| for _rpm in self.rpms[arch]: |
| yield _rpm |
| |
| def get_sibling_debuginfo(self, rpm): |
| """Get sibling debuginfo package of given rpm""" |
| if rpm.arch not in self.ancillary_rpms: |
| return None |
| return self.ancillary_rpms[rpm.arch].get('debuginfo') |
| |
| def get_sibling_devel(self, rpm): |
| """Get sibling devel package of given rpm""" |
| if rpm.arch not in self.ancillary_rpms: |
| return None |
| return self.ancillary_rpms[rpm.arch].get('devel') |
| |
| def get_peer_rpm(self, rpm): |
| """Get peer rpm of rpm from this collection""" |
| if rpm.arch not in self.rpms: |
| return None |
| for _rpm in self.rpms[rpm.arch]: |
| if _rpm.is_peer(rpm): |
| return _rpm |
| return None |
| |
| def get_all_debuginfo_rpms(self, rpm_info): |
| """Return a list of descriptors of all the debuginfo RPMs associated |
| to a given RPM. |
| |
| :param: dict rpm_info a dict representing an RPM. This was |
| received from the Koji API, either from listRPMs or getRPM. |
| :return: a list of dicts containing RPM descriptors (dicts) |
| for the debuginfo RPMs associated to rpm_info |
| :retype: dict |
| """ |
| rpm_infos = self.rpms[rpm_info.arch] |
| result = [] |
| for r in rpm_infos: |
| if r.is_debuginfo: |
| result.append(r) |
| return result |
| |
| |
| def generate_comparison_halves(rpm_col1, rpm_col2): |
| """Iterate RPM collection and peer's to generate comparison halves""" |
| for _rpm in rpm_col1.rpms_iter(): |
| if _rpm.is_debuginfo: |
| continue |
| if _rpm.is_devel and not global_config.check_all_subpackages: |
| continue |
| |
| if global_config.self_compare: |
| rpm2 = _rpm |
| else: |
| rpm2 = rpm_col2.get_peer_rpm(_rpm) |
| if rpm2 is None: |
| logger.warning('Peer RPM of {0} is not found.'.format(_rpm.filename)) |
| continue |
| |
| debuginfo_list1 = [] |
| debuginfo_list2 = [] |
| |
| # If this is a *devel* package we are looking at, then get all |
| # the debug info packages associated to with the main package |
| # and stick them into the resulting comparison half. |
| |
| if _rpm.is_devel: |
| debuginfo_list1 = rpm_col1.get_all_debuginfo_rpms(_rpm) |
| else: |
| debuginfo_list1.append(rpm_col1.get_sibling_debuginfo(_rpm)) |
| |
| |
| devel1 = rpm_col1.get_sibling_devel(_rpm) |
| |
| if global_config.self_compare: |
| debuginfo_list2 = debuginfo_list1 |
| devel2 = devel1 |
| else: |
| if rpm2.is_devel: |
| debuginfo_list2 = rpm_col2.get_all_debuginfo_rpms(rpm2) |
| else: |
| debuginfo_list2.append(rpm_col2.get_sibling_debuginfo(rpm2)) |
| devel2 = rpm_col2.get_sibling_devel(rpm2) |
| |
| yield (ComparisonHalf(subject=_rpm, |
| ancillary_debug=debuginfo_list1, |
| ancillary_devel=devel1), |
| ComparisonHalf(subject=rpm2, |
| ancillary_debug=debuginfo_list2, |
| ancillary_devel=devel2)) |
| |
| |
| class Brew(object): |
| """Interface to Koji XMLRPC API with enhancements specific to fedabipkgdiff |
| |
| kojihub XMLRPC APIs are well-documented in koji's source code. For more |
| details information, please refer to class RootExports within kojihub.py. |
| |
| For details of APIs used within fedabipkgdiff, refer to from line |
| |
| https://pagure.io/koji/blob/master/f/hub/kojihub.py#_7835 |
| """ |
| |
| def __init__(self, baseurl): |
| """Initialize Brew |
| |
| :param str baseurl: the kojihub URL to initialize a session, that is |
| used to access koji XMLRPC APIs. |
| """ |
| self.session = koji.ClientSession(baseurl) |
| |
| @log_call |
| def listRPMs(self, buildID=None, arches=None, selector=None): |
| """Get list of RPMs of a build from Koji |
| |
| Call kojihub.listRPMs to get list of RPMs. Return selected RPMs without |
| changing each RPM information. |
| |
| A RPM returned from listRPMs contains following keys: |
| |
| - id |
| - name |
| - version |
| - release |
| - nvr (synthesized for sorting purposes) |
| - arch |
| - epoch |
| - payloadhash |
| - size |
| - buildtime |
| - build_id |
| - buildroot_id |
| - external_repo_id |
| - external_repo_name |
| - metadata_only |
| - extra |
| |
| :param int buildID: id of a build from which to list RPMs. |
| :param arches: to restrict to list RPMs with specified arches. |
| :type arches: list or tuple |
| :param selector: called to determine if a RPM should be selected and |
| included in the final returned result. Selector must be a callable |
| object and accepts one parameter of a RPM. |
| :type selector: a callable object |
| :return: a list of RPMs, each of them is a dict object |
| :rtype: list |
| """ |
| if selector: |
| assert hasattr(selector, '__call__'), 'selector must be callable.' |
| rpms = self.session.listRPMs(buildID=buildID, arches=arches) |
| if selector: |
| rpms = [rpm for rpm in rpms if selector(rpm)] |
| return rpms |
| |
| @log_call |
| def getRPM(self, rpminfo): |
| """Get a RPM from koji |
| |
| Call kojihub.getRPM, and returns the result directly without any |
| change. |
| |
| When not found a RPM, koji.getRPM will return None, then |
| this method will raise RpmNotFound error immediately to claim what is |
| happening. I want to raise fedabipkgdiff specific error rather than |
| koji's GenericError and then raise RpmNotFound again, so I just simply |
| don't use strict parameter to call koji.getRPM. |
| |
| :param rpminfo: rpminfo may be a N-V-R.A or a map containing name, |
| version, release, and arch. For example, file-5.25-5.fc24.x86_64, and |
| `{'name': 'file', 'version': '5.25', 'release': '5.fc24', 'arch': |
| 'x86_64'}`. |
| :type rpminfo: str or dict |
| :return: a map containing RPM information, that contains same keys as |
| method `Brew.listRPMs`. |
| :rtype: dict |
| :raises RpmNotFound: if a RPM cannot be found with rpminfo. |
| """ |
| rpm = self.session.getRPM(rpminfo) |
| if rpm is None: |
| raise RpmNotFound('Cannot find RPM {0}'.format(rpminfo)) |
| return rpm |
| |
| @log_call |
| def listBuilds(self, packageID, state=None, topone=None, |
| selector=None, order_by=None, reverse=None): |
| """Get list of builds from Koji |
| |
| Call kojihub.listBuilds, and return selected builds without changing |
| each build information. |
| |
| By default, only builds with COMPLETE state are queried and returns |
| afterwards. |
| |
| :param int packageID: id of package to list builds from. |
| :param int state: build state. There are five states of a build in |
| Koji. fedabipkgdiff only cares about builds with COMPLETE state. If |
| state is omitted, builds with COMPLETE state are queried from Koji by |
| default. |
| :param bool topone: just return the top first build. |
| :param selector: a callable object used to select specific subset of |
| builds. Selector will be called immediately after Koji returns queried |
| builds. When each call to selector, a build is passed to |
| selector. Return True if select current build, False if not. |
| :type selector: a callable object |
| :param str order_by: the attribute name by which to order the builds, |
| for example, name, version, or nvr. |
| :param bool reverse: whether to order builds reversely. |
| :return: a list of builds, even if there is only one build. |
| :rtype: list |
| :raises TypeError: if selector is not callable, or if order_by is not a |
| string value. |
| """ |
| if state is None: |
| state = koji.BUILD_STATES['COMPLETE'] |
| |
| if selector is not None and not hasattr(selector, '__call__'): |
| raise TypeError( |
| '{0} is not a callable object.'.format(str(selector))) |
| |
| if order_by is not None and not isinstance(order_by, six.string_types): |
| raise TypeError('order_by {0} is invalid.'.format(order_by)) |
| |
| builds = self.session.listBuilds(packageID=packageID, state=state) |
| if selector is not None: |
| builds = [build for build in builds if selector(build)] |
| if order_by is not None: |
| # FIXME: is it possible to sort builds by using opts parameter of |
| # listBuilds |
| if order_by == 'nvr': |
| if six.PY2: |
| builds = sorted(builds, cmp=cmp_nvr, reverse=reverse) |
| else: |
| builds = sorted(builds, |
| key=functools.cmp_to_key(cmp_nvr), |
| reverse=reverse) |
| else: |
| builds = sorted( |
| builds, key=lambda b: b[order_by], reverse=reverse) |
| if topone: |
| builds = builds[0:1] |
| |
| return builds |
| |
| @log_call |
| def getPackage(self, name): |
| """Get a package from Koji |
| |
| :param str name: a package name. |
| :return: a mapping containing package information. For example, |
| `{'id': 1, 'name': 'package'}`. |
| :rtype: dict |
| """ |
| package = self.session.getPackage(name) |
| if package is None: |
| package = self.session.getPackage(name.rsplit('-', 1)[0]) |
| if package is None: |
| raise KojiPackageNotFound( |
| 'Cannot find package {0}.'.format(name)) |
| return package |
| |
| @log_call |
| def getBuild(self, buildID): |
| """Get a build from Koji |
| |
| Call kojihub.getBuild. Return got build directly without change. |
| |
| :param int buildID: id of build to get from Koji. |
| :return: the found build. Return None, if not found a build with |
| buildID. |
| :rtype: dict |
| """ |
| return self.session.getBuild(buildID) |
| |
| @log_call |
| def get_rpm_build_id(self, name, version, release, arch=None): |
| """Get build ID that contains a RPM with specific nvra |
| |
| If arch is not omitted, a RPM can be identified by its N-V-R-A. |
| |
| If arch is omitted, name is used to get associated package, and then |
| to get the build. |
| |
| Example: |
| |
| >>> brew = Brew('url to kojihub') |
| >>> brew.get_rpm_build_id('httpd', '2.4.18', '2.fc24') |
| >>> brew.get_rpm_build_id('httpd', '2.4.18', '2.fc25', 'x86_64') |
| |
| :param str name: name of a rpm |
| :param str version: version of a rpm |
| :param str release: release of a rpm |
| :param arch: arch of a rpm |
| :type arch: str or None |
| :return: id of the build from where the RPM is built |
| :rtype: dict |
| :raises KojiPackageNotFound: if name is not found from Koji if arch |
| is None. |
| """ |
| if arch is None: |
| package = self.getPackage(name) |
| selector = lambda item: item['version'] == version and \ |
| item['release'] == release |
| builds = self.listBuilds(packageID=package['id'], |
| selector=selector) |
| if not builds: |
| raise NoBuildsError( |
| 'No builds are selected from package {0}.'.format( |
| package['name'])) |
| return builds[0]['build_id'] |
| else: |
| rpm = self.getRPM({'name': name, |
| 'version': version, |
| 'release': release, |
| 'arch': arch, |
| }) |
| return rpm['build_id'] |
| |
| @log_call |
| def get_package_latest_build(self, package_name, distro): |
| """Get latest build from a package, for a particular distro. |
| |
| Example: |
| |
| >>> brew = Brew('url to kojihub') |
| >>> brew.get_package_latest_build('httpd', 'fc24') |
| |
| :param str package_name: from which package to get the latest build |
| :param str distro: which distro the latest build belongs to |
| :return: the found build |
| :rtype: dict or None |
| :raises NoCompleteBuilds: if there is no latest build of a package. |
| """ |
| package = self.getPackage(package_name) |
| selector = lambda item: item['release'].find(distro) > -1 |
| |
| builds = self.listBuilds(packageID=package['id'], |
| selector=selector, |
| order_by='nvr', |
| reverse=True) |
| if not builds: |
| # So we found no build which distro string exactly matches |
| # the 'distro' parameter. |
| # |
| # Now lets try to get builds which distro string are less |
| # than the value of the 'distro' parameter. This is for |
| # cases when, for instance, the build of package foo that |
| # is present in current Fedora 27 is foo-1.fc26. That |
| # build originates from Fedora 26 but is being re-used in |
| # Fedora 27. So we want this function to pick up that |
| # foo-1.fc26, even though we want the builds of foo that |
| # match the distro string fc27. |
| |
| selector = lambda build: get_distro_from_string(build['release']) and \ |
| get_distro_from_string(build['release']) <= distro |
| |
| builds = self.listBuilds(packageID=package['id'], |
| selector=selector, |
| order_by='nvr', |
| reverse=True); |
| |
| if not builds: |
| raise NoCompleteBuilds( |
| 'No complete builds of package {0}'.format(package_name)) |
| |
| return builds[0] |
| |
| @log_call |
| def select_rpms_from_a_build(self, build_id, package_name, arches=None, |
| select_subpackages=None): |
| """Select specific RPMs within a build |
| |
| RPMs could be filtered be specific criterias by the parameters. |
| |
| By default, fedabipkgdiff requires the RPM package, as well as |
| its associated debuginfo and devel packages. These three |
| packages are selected, and noarch and src are excluded. |
| |
| :param int build_id: from which build to select rpms. |
| :param str package_name: which rpm to select that matches this name. |
| :param arches: which arches to select. If arches omits, rpms with all |
| arches except noarch and src will be selected. |
| :type arches: list, tuple or None |
| :param bool select_subpackages: indicate whether to select all RPMs |
| with specific arch from build. |
| :return: a list of RPMs returned from listRPMs |
| :rtype: list |
| """ |
| excluded_arches = ('noarch', 'src') |
| |
| def rpms_selector(package_name, excluded_arches): |
| return lambda rpm: \ |
| rpm['arch'] not in excluded_arches and \ |
| (rpm['name'] == package_name or |
| rpm['name'].endswith('-debuginfo') or |
| rpm['name'].endswith('-devel')) |
| |
| if select_subpackages: |
| selector = lambda rpm: rpm['arch'] not in excluded_arches |
| else: |
| selector = rpms_selector(package_name, excluded_arches) |
| rpm_infos = self.listRPMs(buildID=build_id, |
| arches=arches, |
| selector=selector) |
| return RPMCollection((RPM(rpm_info) for rpm_info in rpm_infos)) |
| |
| @log_call |
| def get_latest_built_rpms(self, package_name, distro, arches=None): |
| """Get RPMs from latest build of a package |
| |
| :param str package_name: from which package to get the rpms |
| :param str distro: which distro the rpms belong to |
| :param arches: which arches the rpms belong to |
| :type arches: str or None |
| :return: the selected RPMs |
| :rtype: list |
| """ |
| latest_build = self.get_package_latest_build(package_name, distro) |
| # Get rpm and debuginfo rpm from each arch |
| return self.select_rpms_from_a_build(latest_build['build_id'], |
| package_name, |
| arches=arches) |
| |
| |
| @log_call |
| def get_session(): |
| """Get instance of Brew to talk with Koji""" |
| return Brew(global_config.koji_server) |
| |
| |
| @log_call |
| def get_download_dir(): |
| """Return the directory holding all downloaded RPMs |
| |
| If directory does not exist, it is created automatically. |
| |
| :return: path to directory holding downloaded RPMs. |
| :rtype: str |
| """ |
| download_dir = os.path.join(HOME_DIR, 'downloads') |
| if not os.path.exists(download_dir): |
| os.makedirs(download_dir) |
| return download_dir |
| |
| |
| @log_call |
| def download_rpm(url): |
| """Using curl to download a RPM from Koji |
| |
| Currently, curl is called and runs in a spawned process. pycurl would be a |
| good way instead. This would be changed in the future. |
| |
| :param str url: URL of a RPM to download. |
| :return: True if a RPM is downloaded successfully, False otherwise. |
| :rtype: bool |
| """ |
| cmd = 'curl --location --silent {0} -o {1}'.format( |
| url, os.path.join(get_download_dir(), |
| os.path.basename(url))) |
| if global_config.dry_run: |
| print('DRY-RUN: {0}'.format(cmd)) |
| return |
| |
| return_code = subprocess.call(cmd, shell=True) |
| if return_code > 0: |
| logger.error('curl fails with returned code: %d.', return_code) |
| return False |
| return True |
| |
| |
| @log_call |
| def download_rpms(rpms): |
| """Download RPMs |
| |
| :param list rpms: list of RPMs to download. |
| """ |
| def _download(rpm): |
| if rpm.is_downloaded: |
| logger.debug('Reuse %s', rpm.downloaded_file) |
| else: |
| logger.debug('Download %s', rpm.download_url) |
| download_rpm(rpm.download_url) |
| |
| for rpm in rpms: |
| _download(rpm) |
| |
| |
| @log_call |
| def build_path_to_abipkgdiff(): |
| """Build the path to the 'abipkgidiff' program to use. |
| |
| The path to 'abipkgdiff' is either the argument of the |
| --abipkgdiff command line option, or the path to 'abipkgdiff' as |
| found in the $PATH environment variable. |
| |
| :return: str a string representing the path to the 'abipkgdiff' |
| command. |
| """ |
| if global_config.abipkgdiff: |
| return global_config.abipkgdiff |
| return DEFAULT_ABIPKGDIFF |
| |
| |
| def format_debug_info_pkg_options(option, debuginfo_list): |
| """Given a list of debug info package descriptors return an option |
| string that looks like: |
| |
| option dbg.rpm1 option dbgrpm2 ... |
| |
| :param: list debuginfo_list a list of instances of the RPM class |
| representing the debug info rpms to use to construct the option |
| string. |
| |
| :return: str a string representing the option string that |
| concatenate the 'option' parameter before the path to each RPM |
| contained in 'debuginfo_list'. |
| """ |
| options = [] |
| |
| for dbg_pkg in debuginfo_list: |
| if dbg_pkg and dbg_pkg.downloaded_file: |
| options.append(' {0} {1}'.format(option, dbg_pkg.downloaded_file)) |
| |
| return ' '.join(options) if options else '' |
| |
| @log_call |
| def abipkgdiff(cmp_half1, cmp_half2): |
| """Run abipkgdiff against found two RPM packages |
| |
| Construct and execute abipkgdiff to get ABI diff |
| |
| abipkgdiff \ |
| --d1 package1-debuginfo --d2 package2-debuginfo \ |
| package1-rpm package2-rpm |
| |
| Output to stdout or stderr from abipkgdiff is not captured. abipkgdiff is |
| called synchronously. fedabipkgdiff does not return until underlying |
| abipkgdiff finishes. |
| |
| :param ComparisonHalf cmp_half1: the first comparison half. |
| :param ComparisonHalf cmp_half2: the second comparison half. |
| :return: return code of underlying abipkgdiff execution. |
| :rtype: int |
| """ |
| abipkgdiff_tool = build_path_to_abipkgdiff() |
| |
| suppressions = '' |
| |
| if global_config.suppr: |
| suppressions = '--suppressions {0}'.format(global_config.suppr) |
| |
| if global_config.no_devel_pkg: |
| devel_pkg1 = '' |
| devel_pkg2 = '' |
| else: |
| if cmp_half1.ancillary_devel is None: |
| msg = 'Development package for {0} does not exist.'.format(cmp_half1.subject.filename) |
| if global_config.error_on_warning: |
| raise RuntimeError(msg) |
| else: |
| devel_pkg1 = '' |
| logger.warning('{0} Ignored.'.format(msg)) |
| else: |
| devel_pkg1 = '--devel-pkg1 {0}'.format(cmp_half1.ancillary_devel.downloaded_file) |
| |
| if cmp_half2.ancillary_devel is None: |
| msg = 'Development package for {0} does not exist.'.format(cmp_half2.subject.filename) |
| if global_config.error_on_warning: |
| raise RuntimeError(msg) |
| else: |
| devel_pkg2 = '' |
| logger.warning('{0} Ignored.'.format(msg)) |
| else: |
| devel_pkg2 = '--devel-pkg2 {0}'.format(cmp_half2.ancillary_devel.downloaded_file) |
| |
| if cmp_half1.ancillary_debug is None: |
| msg = 'Debuginfo package for {0} does not exist.'.format(cmp_half1.subject.filename) |
| if global_config.error_on_warning: |
| raise RuntimeError(msg) |
| else: |
| debuginfo_pkg1 = '' |
| logger.warning('{0} Ignored.'.format(msg)) |
| else: |
| debuginfo_pkg1 = format_debug_info_pkg_options("--d1", cmp_half1.ancillary_debug) |
| |
| if cmp_half2.ancillary_debug is None: |
| msg = 'Debuginfo package for {0} does not exist.'.format(cmp_half2.subject.filename) |
| if global_config.error_on_warning: |
| raise RuntimeError(msg) |
| else: |
| debuginfo_pkg2 = '' |
| logger.warning('{0} Ignored.'.format(msg)) |
| else: |
| debuginfo_pkg2 = format_debug_info_pkg_options("--d2", cmp_half2.ancillary_debug); |
| |
| cmd = [ |
| abipkgdiff_tool, |
| suppressions, |
| '--show-identical-binaries' if global_config.show_identical_binaries else '', |
| '--no-default-suppression' if global_config.no_default_suppr else '', |
| '--dso-only' if global_config.dso_only else '', |
| debuginfo_pkg1, |
| debuginfo_pkg2, |
| devel_pkg1, |
| devel_pkg2, |
| cmp_half1.subject.downloaded_file, |
| cmp_half2.subject.downloaded_file, |
| ] |
| cmd = [s for s in cmd if s != ''] |
| |
| if global_config.dry_run: |
| print('DRY-RUN: {0}'.format(' '.join(cmd))) |
| return |
| |
| logger.debug('Run: %s', ' '.join(cmd)) |
| |
| print('Comparing the ABI of binaries between {0} and {1}:'.format( |
| cmp_half1.subject.filename, cmp_half2.subject.filename)) |
| print() |
| |
| proc = subprocess.Popen(' '.join(cmd), shell=True, |
| stdout=subprocess.PIPE, stderr=subprocess.PIPE, |
| universal_newlines=True) |
| # So we could have done: stdout, stderr = proc.communicate() |
| # But then the documentatin of proc.communicate says: |
| # |
| # Note: The data read is buffered in memory, so do not use this |
| # method if the data size is large or unlimited. " |
| # |
| # In practice, we are seeing random cases where this |
| # proc.communicate() function does *NOT* terminate and seems to be |
| # in a deadlock state. So we are avoiding it altogether. We are |
| # then busy looping, waiting for the spawn process to finish, and |
| # then we get its output. |
| # |
| |
| while True: |
| if proc.poll() != None: |
| break |
| |
| stdout = ''.join(proc.stdout.readlines()) |
| stderr = ''.join(proc.stderr.readlines()) |
| |
| is_ok = proc.returncode == ABIDIFF_OK |
| is_internal_error = proc.returncode & ABIDIFF_ERROR or proc.returncode & ABIDIFF_USAGE_ERROR |
| has_abi_change = proc.returncode & ABIDIFF_ABI_CHANGE |
| |
| if is_internal_error: |
| six.print_(stderr, file=sys.stderr) |
| elif is_ok or has_abi_change: |
| print(stdout) |
| |
| return proc.returncode |
| |
| |
| @log_call |
| def run_abipkgdiff(rpm_col1, rpm_col2): |
| """Run abipkgdiff |
| |
| If one of the executions finds ABI differences, the return code is the |
| return code from abipkgdiff. |
| |
| :param RPMCollection rpm_col1: a collection of RPMs |
| :param RPMCollection rpm_col2: same as rpm_col1 |
| :return: exit code of the last non-zero returned from underlying abipkgdiff |
| :rtype: int |
| """ |
| return_codes = [ |
| abipkgdiff(cmp_half1, cmp_half2) for cmp_half1, cmp_half2 |
| in generate_comparison_halves(rpm_col1, rpm_col2)] |
| return max(return_codes) if return_codes else 0 |
| |
| |
| @log_call |
| def diff_local_rpm_with_latest_rpm_from_koji(): |
| """Diff against local rpm and remove latest rpm |
| |
| This operation handles a local rpm and debuginfo rpm and remote ones |
| located in remote Koji server, that has specific distro specificed by |
| argument --from. |
| |
| 1/ Suppose the packager has just locally built a package named |
| foo-3.0.fc24.rpm. To compare the ABI of this locally build package with the |
| latest stable package from Fedora 23, one would do: |
| |
| fedabipkgdiff --from fc23 ./foo-3.0.fc24.rpm |
| """ |
| |
| from_distro = global_config.from_distro |
| if not is_distro_valid(from_distro): |
| raise InvalidDistroError('Invalid distro {0}'.format(from_distro)) |
| |
| local_rpm_file = global_config.NVR[0] |
| if not os.path.exists(local_rpm_file): |
| raise ValueError('{0} does not exist.'.format(local_rpm_file)) |
| |
| local_rpm = LocalRPM(local_rpm_file) |
| rpm_col1 = session.get_latest_built_rpms(local_rpm.name, |
| from_distro, |
| arches=local_rpm.arch) |
| rpm_col2 = RPMCollection.gather_from_dir(local_rpm_file) |
| |
| if global_config.clean_cache_before: |
| delete_download_cache() |
| |
| download_rpms(rpm_col1.rpms_iter()) |
| result = run_abipkgdiff(rpm_col1, rpm_col2) |
| |
| if global_config.clean_cache_after: |
| delete_download_cache() |
| |
| return result |
| |
| |
| @log_call |
| def diff_latest_rpms_based_on_distros(): |
| """abipkgdiff rpms based on two distros |
| |
| 2/ Suppose the packager wants to see how the ABIs of the package foo |
| evolved between fedora 19 and fedora 22. She would thus type the command: |
| |
| fedabipkgdiff --from fc19 --to fc22 foo |
| """ |
| |
| from_distro = global_config.from_distro |
| to_distro = global_config.to_distro |
| |
| if not is_distro_valid(from_distro): |
| raise InvalidDistroError('Invalid distro {0}'.format(from_distro)) |
| |
| if not is_distro_valid(to_distro): |
| raise InvalidDistroError('Invalid distro {0}'.format(to_distro)) |
| |
| package_name = global_config.NVR[0] |
| |
| rpm_col1 = session.get_latest_built_rpms(package_name, |
| distro=global_config.from_distro) |
| rpm_col2 = session.get_latest_built_rpms(package_name, |
| distro=global_config.to_distro) |
| |
| if global_config.clean_cache_before: |
| delete_download_cache() |
| |
| download_rpms(chain(rpm_col1.rpms_iter(), rpm_col2.rpms_iter())) |
| result = run_abipkgdiff(rpm_col1, rpm_col2) |
| |
| if global_config.clean_cache_after: |
| delete_download_cache() |
| |
| return result |
| |
| |
| @log_call |
| def diff_two_nvras_from_koji(): |
| """Diff two nvras from koji |
| |
| The arch probably omits, that means febabipkgdiff will diff all arches. If |
| specificed, the specific arch will be handled. |
| |
| 3/ Suppose the packager wants to compare the ABI of two packages designated |
| by their name and version. She would issue a command like this: |
| |
| fedabipkgdiff foo-1.0.fc19 foo-3.0.fc24 |
| fedabipkgdiff foo-1.0.fc19.i686 foo-1.0.fc24.i686 |
| """ |
| left_rpm = koji.parse_NVRA(global_config.NVR[0]) |
| right_rpm = koji.parse_NVRA(global_config.NVR[1]) |
| |
| if is_distro_valid(left_rpm['arch']) and \ |
| is_distro_valid(right_rpm['arch']): |
| nvr = koji.parse_NVR(global_config.NVR[0]) |
| params1 = (nvr['name'], nvr['version'], nvr['release'], None) |
| |
| nvr = koji.parse_NVR(global_config.NVR[1]) |
| params2 = (nvr['name'], nvr['version'], nvr['release'], None) |
| else: |
| params1 = (left_rpm['name'], |
| left_rpm['version'], |
| left_rpm['release'], |
| left_rpm['arch']) |
| params2 = (right_rpm['name'], |
| right_rpm['version'], |
| right_rpm['release'], |
| right_rpm['arch']) |
| |
| build_id = session.get_rpm_build_id(*params1) |
| rpm_col1 = session.select_rpms_from_a_build( |
| build_id, params1[0], arches=params1[3], |
| select_subpackages=global_config.check_all_subpackages) |
| |
| build_id = session.get_rpm_build_id(*params2) |
| rpm_col2 = session.select_rpms_from_a_build( |
| build_id, params2[0], arches=params2[3], |
| select_subpackages=global_config.check_all_subpackages) |
| |
| if global_config.clean_cache_before: |
| delete_download_cache() |
| |
| download_rpms(chain(rpm_col1.rpms_iter(), rpm_col2.rpms_iter())) |
| result = run_abipkgdiff(rpm_col1, rpm_col2) |
| |
| if global_config.clean_cache_after: |
| delete_download_cache() |
| |
| return result |
| |
| |
| @log_call |
| def self_compare_rpms_from_distro(): |
| """Compare ABI between same package from a distro |
| |
| Doing ABI comparison on self package should return no |
| ABI change and hence return code should be 0. This is useful |
| to ensure that functionality of libabigail itself |
| didn't break. This utility can be invoked like this: |
| |
| fedabipkgdiff --self-compare -a --from fc25 foo |
| """ |
| |
| from_distro = global_config.from_distro |
| |
| if not is_distro_valid(from_distro): |
| raise InvalidDistroError('Invalid distro {0}'.format(from_distro)) |
| |
| package_name = global_config.NVR[0] |
| |
| rpm_col1 = session.get_latest_built_rpms(package_name, |
| distro=global_config.from_distro) |
| |
| if global_config.clean_cache_before: |
| delete_download_cache() |
| |
| download_rpms(rpm_col1.rpms_iter()) |
| result = run_abipkgdiff(rpm_col1, rpm_col1) |
| |
| if global_config.clean_cache_after: |
| delete_download_cache() |
| |
| return result |
| |
| |
| @log_call |
| def diff_from_two_rpm_files(from_rpm_file, to_rpm_file): |
| """Diff two RPM files""" |
| rpm_col1 = RPMCollection.gather_from_dir(from_rpm_file) |
| rpm_col2 = RPMCollection.gather_from_dir(to_rpm_file) |
| if global_config.clean_cache_before: |
| delete_download_cache() |
| download_rpms(chain(rpm_col1.rpms_iter(), rpm_col2.rpms_iter())) |
| result = run_abipkgdiff(rpm_col1, rpm_col2) |
| if global_config.clean_cache_after: |
| delete_download_cache() |
| return result |
| |
| |
| def build_commandline_args_parser(): |
| parser = argparse.ArgumentParser( |
| description='Compare ABI of shared libraries in RPM packages from the ' |
| 'Koji build system') |
| |
| parser.add_argument( |
| 'NVR', |
| nargs='*', |
| help='RPM package N-V-R, N-V-R-A, N, or local RPM ' |
| 'file names with relative or absolute path.') |
| parser.add_argument( |
| '--dry-run', |
| required=False, |
| dest='dry_run', |
| action='store_true', |
| help='Don\'t actually do the work. The commands that should be ' |
| 'run will be sent to stdout.') |
| parser.add_argument( |
| '--from', |
| required=False, |
| metavar='DISTRO', |
| dest='from_distro', |
| help='baseline Fedora distribution name, for example, fc23') |
| parser.add_argument( |
| '--to', |
| required=False, |
| metavar='DISTRO', |
| dest='to_distro', |
| help='Fedora distribution name to compare against the baseline, for ' |
| 'example, fc24') |
| parser.add_argument( |
| '-a', |
| '--all-subpackages', |
| required=False, |
| action='store_true', |
| dest='check_all_subpackages', |
| help='Check all subpackages instead of only the package specificed in ' |
| 'command line.') |
| parser.add_argument( |
| '--dso-only', |
| required=False, |
| action='store_true', |
| dest='dso_only', |
| help='Compare the ABI of shared libraries only. If this option is not ' |
| 'provided, the tool compares the ABI of all ELF binaries.') |
| parser.add_argument( |
| '--debug', |
| required=False, |
| action='store_true', |
| dest='debug', |
| help='show debug output') |
| parser.add_argument( |
| '--traceback', |
| required=False, |
| action='store_true', |
| dest='show_traceback', |
| help='show traceback when there is an exception thrown.') |
| parser.add_argument( |
| '--server', |
| required=False, |
| metavar='URL', |
| dest='koji_server', |
| default=DEFAULT_KOJI_SERVER, |
| help='URL of koji XMLRPC service. Default is {0}'.format( |
| DEFAULT_KOJI_SERVER)) |
| parser.add_argument( |
| '--topurl', |
| required=False, |
| metavar='URL', |
| dest='koji_topurl', |
| default=DEFAULT_KOJI_TOPURL, |
| help='URL for RPM files access') |
| parser.add_argument( |
| '--abipkgdiff', |
| required=False, |
| metavar='ABIPKGDIFF', |
| dest='abipkgdiff', |
| default='', |
| help="The path to the 'abipkgtool' command to use. " |
| "By default use the one found in $PATH.") |
| parser.add_argument( |
| '--suppressions', |
| required=False, |
| metavar='SUPPR', |
| dest='suppr', |
| default='', |
| help='The suppression specification file to use during comparison') |
| parser.add_argument( |
| '--no-default-suppression', |
| required=False, |
| action='store_true', |
| dest='no_default_suppr', |
| help='Do not load default suppression specifications') |
| parser.add_argument( |
| '--no-devel-pkg', |
| required=False, |
| action='store_true', |
| dest='no_devel_pkg', |
| help='Do not compare ABI with development package') |
| parser.add_argument( |
| '--show-identical-binaries', |
| required=False, |
| action='store_true', |
| dest='show_identical_binaries', |
| help='Show information about binaries whose ABI are identical') |
| parser.add_argument( |
| '--error-on-warning', |
| required=False, |
| action='store_true', |
| dest='error_on_warning', |
| help='Raise error instead of warning') |
| parser.add_argument( |
| '--clean-cache', |
| required=False, |
| action=SetCleanCacheAction, |
| dest='clean_cache', |
| default=None, |
| help='A convenient way to clean cache without specifying ' |
| '--clean-cache-before and --clean-cache-after at same time') |
| parser.add_argument( |
| '--clean-cache-before', |
| required=False, |
| action='store_true', |
| dest='clean_cache_before', |
| default=None, |
| help='Clean cache before ABI comparison') |
| parser.add_argument( |
| '--clean-cache-after', |
| required=False, |
| action='store_true', |
| dest='clean_cache_after', |
| default=None, |
| help='Clean cache after ABI comparison') |
| parser.add_argument( |
| '--self-compare', |
| required=False, |
| action='store_true', |
| dest='self_compare', |
| default=None, |
| help='ABI comparison on same package') |
| return parser |
| |
| |
| def main(): |
| parser = build_commandline_args_parser() |
| |
| args = parser.parse_args() |
| |
| global global_config |
| global_config = args |
| |
| global pathinfo |
| pathinfo = koji.PathInfo(topdir=global_config.koji_topurl) |
| |
| global session |
| session = get_session() |
| |
| if global_config.debug: |
| logger.setLevel(logging.DEBUG) |
| |
| logger.debug(args) |
| |
| if global_config.from_distro and global_config.self_compare and \ |
| global_config.NVR: |
| return self_compare_rpms_from_distro() |
| |
| if global_config.from_distro and global_config.to_distro is None and \ |
| global_config.NVR: |
| return diff_local_rpm_with_latest_rpm_from_koji() |
| |
| if global_config.from_distro and global_config.to_distro and \ |
| global_config.NVR: |
| return diff_latest_rpms_based_on_distros() |
| |
| if global_config.from_distro is None and global_config.to_distro is None: |
| if len(global_config.NVR) > 1: |
| left_one = global_config.NVR[0] |
| right_one = global_config.NVR[1] |
| |
| if is_rpm_file(left_one) and is_rpm_file(right_one): |
| return diff_from_two_rpm_files(left_one, right_one) |
| |
| both_nvr = match_nvr(left_one) and match_nvr(right_one) |
| both_nvra = match_nvra(left_one) and match_nvra(right_one) |
| |
| if both_nvr or both_nvra: |
| return diff_two_nvras_from_koji() |
| |
| six.print_('Unknown arguments. Please refer to --help.', file=sys.stderr) |
| return 1 |
| |
| |
| if __name__ == '__main__': |
| try: |
| sys.exit(main()) |
| except KeyboardInterrupt: |
| if global_config is None: |
| raise |
| if global_config.debug: |
| logger.debug('Terminate by user') |
| else: |
| six.print_('Terminate by user', file=sys.stderr) |
| if global_config.show_traceback: |
| raise |
| else: |
| sys.exit(2) |
| except Exception as e: |
| if global_config is None: |
| raise |
| if global_config.debug: |
| logger.debug(str(e)) |
| else: |
| six.print_(str(e), file=sys.stderr) |
| if global_config.show_traceback: |
| raise |
| else: |
| sys.exit(1) |