1e01aa904Sopenharmony_ci#!/usr/bin/env python
2e01aa904Sopenharmony_ci# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
3e01aa904Sopenharmony_ci# -*- coding: utf-8 -*-
4e01aa904Sopenharmony_ci# -*- Mode: Python
5e01aa904Sopenharmony_ci#
6e01aa904Sopenharmony_ci# Copyright (C) 2013-2016 Red Hat, Inc.
7e01aa904Sopenharmony_ci#
8e01aa904Sopenharmony_ci# Author: Chenxiong Qi
9e01aa904Sopenharmony_ci
10e01aa904Sopenharmony_cifrom __future__ import print_function
11e01aa904Sopenharmony_ci
12e01aa904Sopenharmony_ciimport argparse
13e01aa904Sopenharmony_ciimport functools
14e01aa904Sopenharmony_ciimport glob
15e01aa904Sopenharmony_ciimport logging
16e01aa904Sopenharmony_ciimport mimetypes
17e01aa904Sopenharmony_ciimport os
18e01aa904Sopenharmony_ciimport re
19e01aa904Sopenharmony_ciimport shutil
20e01aa904Sopenharmony_ciimport six
21e01aa904Sopenharmony_ciimport subprocess
22e01aa904Sopenharmony_ciimport sys
23e01aa904Sopenharmony_ci
24e01aa904Sopenharmony_cifrom collections import namedtuple
25e01aa904Sopenharmony_cifrom itertools import chain
26e01aa904Sopenharmony_ci
27e01aa904Sopenharmony_ciimport xdg.BaseDirectory
28e01aa904Sopenharmony_ci
29e01aa904Sopenharmony_ciimport rpm
30e01aa904Sopenharmony_ciimport koji
31e01aa904Sopenharmony_ci
32e01aa904Sopenharmony_ci# @file
33e01aa904Sopenharmony_ci#
34e01aa904Sopenharmony_ci# You might have known that abipkgdiff is a command line tool to compare two
35e01aa904Sopenharmony_ci# RPM packages to find potential differences of ABI. This is really useful for
36e01aa904Sopenharmony_ci# Fedora packagers and developers. Usually, excpet the RPM packages built
37e01aa904Sopenharmony_ci# locally, if a packager wants to compare RPM packages he just built with
38e01aa904Sopenharmony_ci# specific RPM packages that were already built and availabe in Koji,
39e01aa904Sopenharmony_ci# fedabipkgdiff is the right tool for him.
40e01aa904Sopenharmony_ci#
41e01aa904Sopenharmony_ci# With fedabipkgdiff, packager is able to specify certain criteria to tell
42e01aa904Sopenharmony_ci# fedabipkgdiff which RPM packages he wants to compare, then fedabipkgdiff will
43e01aa904Sopenharmony_ci# find them, download them, and boom, run the abipkgdiff for you.
44e01aa904Sopenharmony_ci#
45e01aa904Sopenharmony_ci# Currently, fedabipkgdiff returns 0 if everything works well, otherwise, 1 if
46e01aa904Sopenharmony_ci# something wrong.
47e01aa904Sopenharmony_ci
48e01aa904Sopenharmony_ci
49e01aa904Sopenharmony_ci# First, try proper Koji initialization.
50e01aa904Sopenharmony_citry:
51e01aa904Sopenharmony_ci    koji_config = koji.read_config('koji')
52e01aa904Sopenharmony_ci    DEFAULT_KOJI_SERVER = koji_config['server']
53e01aa904Sopenharmony_ci    DEFAULT_KOJI_TOPURL = koji_config['topurl']
54e01aa904Sopenharmony_ciexcept koji.ConfigurationError:
55e01aa904Sopenharmony_ci    # ..., but if that fails because of a rather strict interpretation where
56e01aa904Sopenharmony_ci    # 'read_config' looks for configuration files, just use dummy values.
57e01aa904Sopenharmony_ci    # These fail upon use unless overridden, which for libabigail test suite
58e01aa904Sopenharmony_ci    # usage they always are (all relevant artifacts are shipped in the
59e01aa904Sopenharmony_ci    # libabigail distribution).
60e01aa904Sopenharmony_ci    DEFAULT_KOJI_SERVER = 'dummy_DEFAULT_KOJI_SERVER'
61e01aa904Sopenharmony_ci    DEFAULT_KOJI_TOPURL = 'dummy_DEFAULT_KOJI_TOPURL'
62e01aa904Sopenharmony_ci
63e01aa904Sopenharmony_ci
64e01aa904Sopenharmony_ci# The working directory where to hold all data including downloaded RPM
65e01aa904Sopenharmony_ci# packages Currently, it's not configurable and hardcode here. In the future
66e01aa904Sopenharmony_ci# version of fedabipkgdiff, I'll make it configurable by users.
67e01aa904Sopenharmony_ciHOME_DIR = os.path.join(xdg.BaseDirectory.xdg_cache_home,
68e01aa904Sopenharmony_ci                        os.path.splitext(os.path.basename(__file__))[0])
69e01aa904Sopenharmony_ci
70e01aa904Sopenharmony_ciDEFAULT_ABIPKGDIFF = 'abipkgdiff'
71e01aa904Sopenharmony_ci
72e01aa904Sopenharmony_ci# Mask for determining if underlying fedabipkgdiff succeeds or not.
73e01aa904Sopenharmony_ci# This is for when the compared ABIs are equal
74e01aa904Sopenharmony_ciABIDIFF_OK = 0
75e01aa904Sopenharmony_ci# This bit is set if there an application error.
76e01aa904Sopenharmony_ciABIDIFF_ERROR = 1
77e01aa904Sopenharmony_ci# This bit is set if the tool is invoked in an non appropriate manner.
78e01aa904Sopenharmony_ciABIDIFF_USAGE_ERROR = 1 << 1
79e01aa904Sopenharmony_ci# This bit is set if the ABIs being compared are different.
80e01aa904Sopenharmony_ciABIDIFF_ABI_CHANGE = 1 << 2
81e01aa904Sopenharmony_ci
82e01aa904Sopenharmony_ci
83e01aa904Sopenharmony_ci# Used to construct abipkgdiff command line argument, package and associated
84e01aa904Sopenharmony_ci# debuginfo package
85e01aa904Sopenharmony_ci# fedabipkgdiff runs abipkgdiff in this form
86e01aa904Sopenharmony_ci#
87e01aa904Sopenharmony_ci#   abipkgdiff \
88e01aa904Sopenharmony_ci#       --d1 /path/to/package1-debuginfo.rpm \
89e01aa904Sopenharmony_ci#       --d2 /path/to/package2-debuginfo.rpm \
90e01aa904Sopenharmony_ci#       /path/to/package1.rpm \
91e01aa904Sopenharmony_ci#       /path/to/package2.rpm
92e01aa904Sopenharmony_ci#
93e01aa904Sopenharmony_ci# ComparisonHalf is a three-elements tuple in format
94e01aa904Sopenharmony_ci#
95e01aa904Sopenharmony_ci#   (package1.rpm, [package1-debuginfo.rpm..] package1-devel.rpm)
96e01aa904Sopenharmony_ci#
97e01aa904Sopenharmony_ci# - the first element is the subject representing the package to
98e01aa904Sopenharmony_ci#   compare.  It's a dict representing the RPM we are interested in.
99e01aa904Sopenharmony_ci#   That dict was retrieved from Koji XMLRPC API.
100e01aa904Sopenharmony_ci# - the rest are ancillary packages used for the comparison. So, the
101e01aa904Sopenharmony_ci#   second one is a vector containing the needed debuginfo packages
102e01aa904Sopenharmony_ci#   (yes there can be more than one), and the last one is the package
103e01aa904Sopenharmony_ci#   containing API of the ELF shared libraries carried by subject.
104e01aa904Sopenharmony_ci#   All the packages are dicts representing RPMs and those dicts were
105e01aa904Sopenharmony_ci#   retrieved fromt he KOji XMLRPC API.
106e01aa904Sopenharmony_ci#
107e01aa904Sopenharmony_ci# So, before calling abipkgdiff, fedabipkgdiff must prepare and pass
108e01aa904Sopenharmony_ci# the following information
109e01aa904Sopenharmony_ci#
110e01aa904Sopenharmony_ci#   (/path/to/package1.rpm, [/paths/to/package1-debuginfo.rpm ..] /path/to/package1-devel.rpm)
111e01aa904Sopenharmony_ci#   (/path/to/package2.rpm, [/paths/to/package2-debuginfo.rpm ..] /path/to/package1-devel.rpm)
112e01aa904Sopenharmony_ci#
113e01aa904Sopenharmony_ciComparisonHalf = namedtuple('ComparisonHalf',
114e01aa904Sopenharmony_ci                            ['subject', 'ancillary_debug', 'ancillary_devel'])
115e01aa904Sopenharmony_ci
116e01aa904Sopenharmony_ci
117e01aa904Sopenharmony_ciglobal_config = None
118e01aa904Sopenharmony_cipathinfo = None
119e01aa904Sopenharmony_cisession = None
120e01aa904Sopenharmony_ci
121e01aa904Sopenharmony_ci# There is no way to configure the log format so far. I hope I would have time
122e01aa904Sopenharmony_ci# to make it available so that if fedabipkgdiff is scheduled and run by some
123e01aa904Sopenharmony_ci# service, the logs logged into log file is muc usable.
124e01aa904Sopenharmony_cilogging.basicConfig(format='[%(levelname)s] %(message)s', level=logging.CRITICAL)
125e01aa904Sopenharmony_cilogger = logging.getLogger(os.path.basename(__file__))
126e01aa904Sopenharmony_ci
127e01aa904Sopenharmony_ci
128e01aa904Sopenharmony_ciclass KojiPackageNotFound(Exception):
129e01aa904Sopenharmony_ci    """Package is not found in Koji"""
130e01aa904Sopenharmony_ci
131e01aa904Sopenharmony_ci
132e01aa904Sopenharmony_ciclass PackageNotFound(Exception):
133e01aa904Sopenharmony_ci    """Package is not found locally"""
134e01aa904Sopenharmony_ci
135e01aa904Sopenharmony_ci
136e01aa904Sopenharmony_ciclass RpmNotFound(Exception):
137e01aa904Sopenharmony_ci    """RPM is not found"""
138e01aa904Sopenharmony_ci
139e01aa904Sopenharmony_ci
140e01aa904Sopenharmony_ciclass NoBuildsError(Exception):
141e01aa904Sopenharmony_ci    """No builds returned from a method to select specific builds"""
142e01aa904Sopenharmony_ci
143e01aa904Sopenharmony_ci
144e01aa904Sopenharmony_ciclass NoCompleteBuilds(Exception):
145e01aa904Sopenharmony_ci    """No complete builds for a package
146e01aa904Sopenharmony_ci
147e01aa904Sopenharmony_ci    This is a serious problem, nothing can be done if there is no complete
148e01aa904Sopenharmony_ci    builds for a package.
149e01aa904Sopenharmony_ci    """
150e01aa904Sopenharmony_ci
151e01aa904Sopenharmony_ci
152e01aa904Sopenharmony_ciclass InvalidDistroError(Exception):
153e01aa904Sopenharmony_ci    """Invalid distro error"""
154e01aa904Sopenharmony_ci
155e01aa904Sopenharmony_ci
156e01aa904Sopenharmony_ciclass CannotFindLatestBuildError(Exception):
157e01aa904Sopenharmony_ci    """Cannot find latest build from a package"""
158e01aa904Sopenharmony_ci
159e01aa904Sopenharmony_ci
160e01aa904Sopenharmony_ciclass SetCleanCacheAction(argparse._StoreTrueAction):
161e01aa904Sopenharmony_ci    """Custom Action making clean-cache as bundle of clean-cache-before and clean-cache-after"""
162e01aa904Sopenharmony_ci
163e01aa904Sopenharmony_ci    def __call__(self, parser, namespace, values, option_string=None):
164e01aa904Sopenharmony_ci        setattr(namespace, 'clean_cache_before', self.const)
165e01aa904Sopenharmony_ci        setattr(namespace, 'clean_cache_after', self.const)
166e01aa904Sopenharmony_ci
167e01aa904Sopenharmony_ci
168e01aa904Sopenharmony_cidef is_distro_valid(distro):
169e01aa904Sopenharmony_ci    """Adjust if a distro is valid
170e01aa904Sopenharmony_ci
171e01aa904Sopenharmony_ci    Currently, check for Fedora and RHEL.
172e01aa904Sopenharmony_ci
173e01aa904Sopenharmony_ci    :param str distro: a string representing a distro value.
174e01aa904Sopenharmony_ci    :return: True if distro is the one specific to Fedora, like fc24, el7.
175e01aa904Sopenharmony_ci    "rtype: bool
176e01aa904Sopenharmony_ci    """
177e01aa904Sopenharmony_ci    return re.match(r'^(fc|el)\d{1,2}$', distro) is not None
178e01aa904Sopenharmony_ci
179e01aa904Sopenharmony_ci
180e01aa904Sopenharmony_cidef get_distro_from_string(str):
181e01aa904Sopenharmony_ci    """Get the part of a string that designates the Fedora distro version number
182e01aa904Sopenharmony_ci
183e01aa904Sopenharmony_ci    For instance, when passed the string '2.3.fc12', this function
184e01aa904Sopenharmony_ci    returns the string 'fc12'.
185e01aa904Sopenharmony_ci
186e01aa904Sopenharmony_ci    :param str the string to consider
187e01aa904Sopenharmony_ci    :return: The sub-string of the parameter that represents the
188e01aa904Sopenharmony_ci    Fedora distro version number, or None if the parameter does not
189e01aa904Sopenharmony_ci    contain such a sub-string.
190e01aa904Sopenharmony_ci    """
191e01aa904Sopenharmony_ci
192e01aa904Sopenharmony_ci    m = re.match(r'(.*)((fc|el)\d{1,2})(.*)', str)
193e01aa904Sopenharmony_ci    if not m:
194e01aa904Sopenharmony_ci        return None
195e01aa904Sopenharmony_ci
196e01aa904Sopenharmony_ci    distro = m.group(2)
197e01aa904Sopenharmony_ci    return distro
198e01aa904Sopenharmony_ci
199e01aa904Sopenharmony_ci
200e01aa904Sopenharmony_cidef match_nvr(s):
201e01aa904Sopenharmony_ci    """Determine if a string is a N-V-R"""
202e01aa904Sopenharmony_ci    return re.match(r'^([^/]+)-(.+)-(.+)$', s) is not None
203e01aa904Sopenharmony_ci
204e01aa904Sopenharmony_ci
205e01aa904Sopenharmony_cidef match_nvra(s):
206e01aa904Sopenharmony_ci    """Determine if a string is a N-V-R.A"""
207e01aa904Sopenharmony_ci    return re.match(r'^([^/]+)-(.+)-(.+)\.(.+)$', s) is not None
208e01aa904Sopenharmony_ci
209e01aa904Sopenharmony_ci
210e01aa904Sopenharmony_cidef is_rpm_file(filename):
211e01aa904Sopenharmony_ci    """Return if a file is a RPM"""
212e01aa904Sopenharmony_ci    isfile = os.path.isfile(filename)
213e01aa904Sopenharmony_ci    mimetype = mimetypes.guess_type(filename)[0] if isfile else None
214e01aa904Sopenharmony_ci    isrpm = (mimetype == 'application/x-redhat-package-manager'
215e01aa904Sopenharmony_ci             or mimetype == 'application/x-rpm')
216e01aa904Sopenharmony_ci
217e01aa904Sopenharmony_ci    # Most systems won't have rpm defined as a mimetype
218e01aa904Sopenharmony_ci    if not mimetype and filename.endswith('.rpm'):
219e01aa904Sopenharmony_ci        isrpm = True
220e01aa904Sopenharmony_ci    logger.debug('is_rpm_file(\'%s\'): isfile=%s, mimetype=\'%s\', isrpm=%s',
221e01aa904Sopenharmony_ci                 filename, isfile, mimetype, isrpm)
222e01aa904Sopenharmony_ci    return isrpm
223e01aa904Sopenharmony_ci
224e01aa904Sopenharmony_ci
225e01aa904Sopenharmony_cidef cmp_nvr(left, right):
226e01aa904Sopenharmony_ci    """Compare function for sorting a sequence of NVRs
227e01aa904Sopenharmony_ci
228e01aa904Sopenharmony_ci    This is the compare function used in sorted function to sort builds so that
229e01aa904Sopenharmony_ci    fedabipkgdiff is able to select the latest build. Return value follows the
230e01aa904Sopenharmony_ci    rules described in the part of paramter cmp of sorted documentation.
231e01aa904Sopenharmony_ci
232e01aa904Sopenharmony_ci    :param str left: left nvr to compare.
233e01aa904Sopenharmony_ci    :param str right: right nvr to compare.
234e01aa904Sopenharmony_ci    :return: -1, 0, or 1 that represents left is considered smaller than,
235e01aa904Sopenharmony_ci    equal to, or larger than the right individually.
236e01aa904Sopenharmony_ci    :rtype: int
237e01aa904Sopenharmony_ci    """
238e01aa904Sopenharmony_ci    left_nvr = koji.parse_NVR(left['nvr'])
239e01aa904Sopenharmony_ci    right_nvr = koji.parse_NVR(right['nvr'])
240e01aa904Sopenharmony_ci    return rpm.labelCompare(
241e01aa904Sopenharmony_ci        (left_nvr['epoch'], left_nvr['version'], left_nvr['release']),
242e01aa904Sopenharmony_ci        (right_nvr['epoch'], right_nvr['version'], right_nvr['release']))
243e01aa904Sopenharmony_ci
244e01aa904Sopenharmony_ci
245e01aa904Sopenharmony_cidef log_call(func):
246e01aa904Sopenharmony_ci    """A decorator that logs a method invocation
247e01aa904Sopenharmony_ci
248e01aa904Sopenharmony_ci    Method's name and all arguments, either positional or keyword arguments,
249e01aa904Sopenharmony_ci    will be logged by logger.debug. Also, return value from the decorated
250e01aa904Sopenharmony_ci    method will be logged just after the invocation is done.
251e01aa904Sopenharmony_ci
252e01aa904Sopenharmony_ci    This decorator does not catch any exception thrown from the decorated
253e01aa904Sopenharmony_ci    method. If there is any exception thrown from decorated method, you can
254e01aa904Sopenharmony_ci    catch them in the caller and obviously, no return value is logged.
255e01aa904Sopenharmony_ci
256e01aa904Sopenharmony_ci    :param callable func: a callable object to decorate
257e01aa904Sopenharmony_ci    """
258e01aa904Sopenharmony_ci    def proxy(*args, **kwargs):
259e01aa904Sopenharmony_ci        logger.debug('Call %s, args: %s, kwargs: %s',
260e01aa904Sopenharmony_ci                     func.__name__,
261e01aa904Sopenharmony_ci                     args if args else '',
262e01aa904Sopenharmony_ci                     kwargs if kwargs else '')
263e01aa904Sopenharmony_ci        result = func(*args, **kwargs)
264e01aa904Sopenharmony_ci        logger.debug('Result from %s: %s', func.__name__, result)
265e01aa904Sopenharmony_ci        return result
266e01aa904Sopenharmony_ci    return proxy
267e01aa904Sopenharmony_ci
268e01aa904Sopenharmony_ci
269e01aa904Sopenharmony_cidef delete_download_cache():
270e01aa904Sopenharmony_ci    """Delete download cache directory"""
271e01aa904Sopenharmony_ci    download_dir = get_download_dir()
272e01aa904Sopenharmony_ci    if global_config.dry_run:
273e01aa904Sopenharmony_ci        print('DRY-RUN: Delete cached downloaded RPM packages at {0}'.format(download_dir))
274e01aa904Sopenharmony_ci    else:
275e01aa904Sopenharmony_ci        logger.debug('Delete cached downloaded RPM packages at {0}'.format(download_dir))
276e01aa904Sopenharmony_ci        shutil.rmtree(download_dir)
277e01aa904Sopenharmony_ci
278e01aa904Sopenharmony_ci
279e01aa904Sopenharmony_ciclass RPM(object):
280e01aa904Sopenharmony_ci    """Wrapper around an RPM descriptor received from Koji
281e01aa904Sopenharmony_ci
282e01aa904Sopenharmony_ci    The RPM descriptor that is returned from Koji XMLRPC API is a
283e01aa904Sopenharmony_ci    dict. This wrapper class makes it eaiser to access all these
284e01aa904Sopenharmony_ci    properties in the way of object.property.
285e01aa904Sopenharmony_ci    """
286e01aa904Sopenharmony_ci
287e01aa904Sopenharmony_ci    def __init__(self, rpm_info):
288e01aa904Sopenharmony_ci        """Initialize a RPM object
289e01aa904Sopenharmony_ci
290e01aa904Sopenharmony_ci        :param dict rpm_info: a dict representing an RPM descriptor
291e01aa904Sopenharmony_ci        received from the Koji API, either listRPMs or getRPM
292e01aa904Sopenharmony_ci        """
293e01aa904Sopenharmony_ci        self.rpm_info = rpm_info
294e01aa904Sopenharmony_ci
295e01aa904Sopenharmony_ci    def __str__(self):
296e01aa904Sopenharmony_ci        """Return the string representation of this RPM
297e01aa904Sopenharmony_ci
298e01aa904Sopenharmony_ci        Return the string representation of RPM information returned from Koji
299e01aa904Sopenharmony_ci        directly so that RPM can be treated in same way.
300e01aa904Sopenharmony_ci        """
301e01aa904Sopenharmony_ci        return str(self.rpm_info)
302e01aa904Sopenharmony_ci
303e01aa904Sopenharmony_ci    def __getattr__(self, name):
304e01aa904Sopenharmony_ci        """Access RPM information in the way of object.property
305e01aa904Sopenharmony_ci
306e01aa904Sopenharmony_ci        :param str name: the property name to access.
307e01aa904Sopenharmony_ci        :raises AttributeError: if name is not one of keys of RPM information.
308e01aa904Sopenharmony_ci        """
309e01aa904Sopenharmony_ci        if name in self.rpm_info:
310e01aa904Sopenharmony_ci            return self.rpm_info[name]
311e01aa904Sopenharmony_ci        else:
312e01aa904Sopenharmony_ci            raise AttributeError('No attribute name {0}'.format(name))
313e01aa904Sopenharmony_ci
314e01aa904Sopenharmony_ci    def is_peer(self, another_rpm):
315e01aa904Sopenharmony_ci        """Determine if this is the peer of a given rpm.
316e01aa904Sopenharmony_ci
317e01aa904Sopenharmony_ci        Here is what "peer" means.
318e01aa904Sopenharmony_ci
319e01aa904Sopenharmony_ci        Consider a package P for which the tripplet Name, Version,
320e01aa904Sopenharmony_ci        Release is made of the values {N,V,R}.  Then, consider a
321e01aa904Sopenharmony_ci        package P' for which the similar tripplet is {N', V', R'}.
322e01aa904Sopenharmony_ci
323e01aa904Sopenharmony_ci        P' is a peer of P if N == N', and either V != V' or R != R'.
324e01aa904Sopenharmony_ci        given package with a given NVR is another package with a N'V'
325e01aa904Sopenharmony_ci        """
326e01aa904Sopenharmony_ci        return self.name == another_rpm.name and \
327e01aa904Sopenharmony_ci            self.arch == another_rpm.arch and \
328e01aa904Sopenharmony_ci            not (self.version == another_rpm.version
329e01aa904Sopenharmony_ci                 and self.release == another_rpm.release)
330e01aa904Sopenharmony_ci
331e01aa904Sopenharmony_ci    @property
332e01aa904Sopenharmony_ci    def nvra(self):
333e01aa904Sopenharmony_ci        """Return a RPM's N-V-R-A representation
334e01aa904Sopenharmony_ci
335e01aa904Sopenharmony_ci        An example: libabigail-1.0-0.8.rc4.1.fc23.x86_64
336e01aa904Sopenharmony_ci        """
337e01aa904Sopenharmony_ci        nvra, _ = os.path.splitext(self.filename)
338e01aa904Sopenharmony_ci        return nvra
339e01aa904Sopenharmony_ci
340e01aa904Sopenharmony_ci    @property
341e01aa904Sopenharmony_ci    def filename(self):
342e01aa904Sopenharmony_ci        """Return a RPM file name
343e01aa904Sopenharmony_ci
344e01aa904Sopenharmony_ci        An example: libabigail-1.0-0.8.rc4.1.fc23.x86_64.rpm
345e01aa904Sopenharmony_ci        """
346e01aa904Sopenharmony_ci        return os.path.basename(pathinfo.rpm(self.rpm_info))
347e01aa904Sopenharmony_ci
348e01aa904Sopenharmony_ci    @property
349e01aa904Sopenharmony_ci    def is_debuginfo(self):
350e01aa904Sopenharmony_ci        """Check if the name of the current RPM denotes a debug info package"""
351e01aa904Sopenharmony_ci        return koji.is_debuginfo(self.rpm_info['name'])
352e01aa904Sopenharmony_ci
353e01aa904Sopenharmony_ci    @property
354e01aa904Sopenharmony_ci    def is_devel(self):
355e01aa904Sopenharmony_ci        """Check if the name of current RPM denotes a development package"""
356e01aa904Sopenharmony_ci        return self.rpm_info['name'].endswith('-devel')
357e01aa904Sopenharmony_ci
358e01aa904Sopenharmony_ci    @property
359e01aa904Sopenharmony_ci    def download_url(self):
360e01aa904Sopenharmony_ci        """Get the URL from where to download this RPM"""
361e01aa904Sopenharmony_ci        build = session.getBuild(self.build_id)
362e01aa904Sopenharmony_ci        return os.path.join(pathinfo.build(build), pathinfo.rpm(self.rpm_info))
363e01aa904Sopenharmony_ci
364e01aa904Sopenharmony_ci    @property
365e01aa904Sopenharmony_ci    def downloaded_file(self):
366e01aa904Sopenharmony_ci        """Get a pridictable downloaded file name with absolute path"""
367e01aa904Sopenharmony_ci        # arch should be removed from the result returned from PathInfo.rpm
368e01aa904Sopenharmony_ci        filename = os.path.basename(pathinfo.rpm(self.rpm_info))
369e01aa904Sopenharmony_ci        return os.path.join(get_download_dir(), filename)
370e01aa904Sopenharmony_ci
371e01aa904Sopenharmony_ci    @property
372e01aa904Sopenharmony_ci    def is_downloaded(self):
373e01aa904Sopenharmony_ci        """Check if this RPM was already downloaded to local disk"""
374e01aa904Sopenharmony_ci        return os.path.exists(self.downloaded_file)
375e01aa904Sopenharmony_ci
376e01aa904Sopenharmony_ci
377e01aa904Sopenharmony_ciclass LocalRPM(RPM):
378e01aa904Sopenharmony_ci    """Representing a local RPM
379e01aa904Sopenharmony_ci
380e01aa904Sopenharmony_ci    Local RPM means the one that could be already downloaded or built from
381e01aa904Sopenharmony_ci    where I can find it
382e01aa904Sopenharmony_ci    """
383e01aa904Sopenharmony_ci
384e01aa904Sopenharmony_ci    def __init__(self, filename):
385e01aa904Sopenharmony_ci        """Initialize local RPM with a filename
386e01aa904Sopenharmony_ci
387e01aa904Sopenharmony_ci        :param str filename: a filename pointing to a RPM file in local
388e01aa904Sopenharmony_ci        disk. Note that, this file must not exist necessarily.
389e01aa904Sopenharmony_ci        """
390e01aa904Sopenharmony_ci        self.local_filename = filename
391e01aa904Sopenharmony_ci        self.rpm_info = koji.parse_NVRA(os.path.basename(filename))
392e01aa904Sopenharmony_ci
393e01aa904Sopenharmony_ci    @property
394e01aa904Sopenharmony_ci    def downloaded_file(self):
395e01aa904Sopenharmony_ci        """Return filename of this RPM
396e01aa904Sopenharmony_ci
397e01aa904Sopenharmony_ci        Returned filename is just the one passed when initializing this RPM.
398e01aa904Sopenharmony_ci
399e01aa904Sopenharmony_ci        :return: filename of this RPM
400e01aa904Sopenharmony_ci        :rtype: str
401e01aa904Sopenharmony_ci        """
402e01aa904Sopenharmony_ci        return self.local_filename
403e01aa904Sopenharmony_ci
404e01aa904Sopenharmony_ci    @property
405e01aa904Sopenharmony_ci    def download_url(self):
406e01aa904Sopenharmony_ci        raise NotImplementedError('LocalRPM has no URL to download')
407e01aa904Sopenharmony_ci
408e01aa904Sopenharmony_ci    def _find_rpm(self, rpm_filename):
409e01aa904Sopenharmony_ci        """Search an RPM from the directory of the current instance of LocalRPM
410e01aa904Sopenharmony_ci
411e01aa904Sopenharmony_ci        :param str rpm_filename: filename of rpm to find, for example
412e01aa904Sopenharmony_ci        foo-devel-0.1-1.fc24.
413e01aa904Sopenharmony_ci        :return: an instance of LocalRPM representing the found rpm, or None if
414e01aa904Sopenharmony_ci        no RPM was found.
415e01aa904Sopenharmony_ci        """
416e01aa904Sopenharmony_ci        search_dir = os.path.dirname(os.path.abspath(self.local_filename))
417e01aa904Sopenharmony_ci        filename = os.path.join(search_dir, rpm_filename)
418e01aa904Sopenharmony_ci        return LocalRPM(filename) if os.path.exists(filename) else None
419e01aa904Sopenharmony_ci
420e01aa904Sopenharmony_ci    @log_call
421e01aa904Sopenharmony_ci    def find_debuginfo(self):
422e01aa904Sopenharmony_ci        """Find debuginfo RPM package from a directory"""
423e01aa904Sopenharmony_ci        filename = \
424e01aa904Sopenharmony_ci            '%(name)s-debuginfo-%(version)s-%(release)s.%(arch)s.rpm' % \
425e01aa904Sopenharmony_ci            self.rpm_info
426e01aa904Sopenharmony_ci        return self._find_rpm(filename)
427e01aa904Sopenharmony_ci
428e01aa904Sopenharmony_ci    @log_call
429e01aa904Sopenharmony_ci    def find_devel(self):
430e01aa904Sopenharmony_ci        """Find development package from a directory"""
431e01aa904Sopenharmony_ci        filename = \
432e01aa904Sopenharmony_ci            '%(name)s-devel-%(version)s-%(release)s.%(arch)s.rpm' % \
433e01aa904Sopenharmony_ci            self.rpm_info
434e01aa904Sopenharmony_ci        return self._find_rpm(filename)
435e01aa904Sopenharmony_ci
436e01aa904Sopenharmony_ci
437e01aa904Sopenharmony_ciclass RPMCollection(object):
438e01aa904Sopenharmony_ci    """Collection of RPMs
439e01aa904Sopenharmony_ci
440e01aa904Sopenharmony_ci    This is a simple collection containing RPMs collected from a
441e01aa904Sopenharmony_ci    directory on the local filesystem or retrieved from Koji.
442e01aa904Sopenharmony_ci
443e01aa904Sopenharmony_ci    A collection can contain one or more sets of RPMs.  Each set of
444e01aa904Sopenharmony_ci    RPMs being for a particular architecture.
445e01aa904Sopenharmony_ci
446e01aa904Sopenharmony_ci    For a given architecture, a set of RPMs is made of one RPM and its
447e01aa904Sopenharmony_ci    ancillary RPMs.  An ancillary RPM is either a debuginfo RPM or a
448e01aa904Sopenharmony_ci    devel RPM.
449e01aa904Sopenharmony_ci
450e01aa904Sopenharmony_ci    So a given RPMCollection would (informally) look like:
451e01aa904Sopenharmony_ci
452e01aa904Sopenharmony_ci    {
453e01aa904Sopenharmony_ci      i686   => {foo.i686.rpm, foo-debuginfo.i686.rpm, foo-devel.i686.rpm}
454e01aa904Sopenharmony_ci      x86_64 => {foo.x86_64.rpm, foo-debuginfo.x86_64.rpm, foo-devel.x86_64.rpm,}
455e01aa904Sopenharmony_ci    }
456e01aa904Sopenharmony_ci
457e01aa904Sopenharmony_ci    """
458e01aa904Sopenharmony_ci
459e01aa904Sopenharmony_ci    def __init__(self, rpms=None):
460e01aa904Sopenharmony_ci        # Mapping from arch to a list of rpm_infos.
461e01aa904Sopenharmony_ci        # Note that *all* RPMs of the collections are present in this
462e01aa904Sopenharmony_ci        # map; that is the RPM to consider and its ancillary RPMs.
463e01aa904Sopenharmony_ci        self.rpms = {}
464e01aa904Sopenharmony_ci
465e01aa904Sopenharmony_ci        # Mapping from arch to another mapping containing index of debuginfo
466e01aa904Sopenharmony_ci        # and development package
467e01aa904Sopenharmony_ci        # e.g.
468e01aa904Sopenharmony_ci        # self.ancillary_rpms = {'i686', {'debuginfo': foo-debuginfo.rpm,
469e01aa904Sopenharmony_ci        #                                 'devel': foo-devel.rpm}}
470e01aa904Sopenharmony_ci        self.ancillary_rpms = {}
471e01aa904Sopenharmony_ci
472e01aa904Sopenharmony_ci        if rpms:
473e01aa904Sopenharmony_ci            for rpm in rpms:
474e01aa904Sopenharmony_ci                self.add(rpm)
475e01aa904Sopenharmony_ci
476e01aa904Sopenharmony_ci    @classmethod
477e01aa904Sopenharmony_ci    def gather_from_dir(cls, rpm_file, all_rpms=None):
478e01aa904Sopenharmony_ci        """Gather RPM collection from local directory"""
479e01aa904Sopenharmony_ci        dir_name = os.path.dirname(os.path.abspath(rpm_file))
480e01aa904Sopenharmony_ci        filename = os.path.basename(rpm_file)
481e01aa904Sopenharmony_ci
482e01aa904Sopenharmony_ci        nvra = koji.parse_NVRA(filename)
483e01aa904Sopenharmony_ci        rpm_files = glob.glob(os.path.join(
484e01aa904Sopenharmony_ci            dir_name, '*-%(version)s-%(release)s.%(arch)s.rpm' % nvra))
485e01aa904Sopenharmony_ci        rpm_col = cls()
486e01aa904Sopenharmony_ci
487e01aa904Sopenharmony_ci        if all_rpms:
488e01aa904Sopenharmony_ci            selector = lambda rpm: True
489e01aa904Sopenharmony_ci        else:
490e01aa904Sopenharmony_ci            selector = lambda rpm: local_rpm.is_devel or \
491e01aa904Sopenharmony_ci                local_rpm.is_debuginfo or local_rpm.filename == filename
492e01aa904Sopenharmony_ci
493e01aa904Sopenharmony_ci        found_debuginfo = 1
494e01aa904Sopenharmony_ci
495e01aa904Sopenharmony_ci        for rpm_file in rpm_files:
496e01aa904Sopenharmony_ci            local_rpm = LocalRPM(rpm_file)
497e01aa904Sopenharmony_ci
498e01aa904Sopenharmony_ci            if local_rpm.is_debuginfo:
499e01aa904Sopenharmony_ci                found_debuginfo <<= 1
500e01aa904Sopenharmony_ci                if found_debuginfo == 4:
501e01aa904Sopenharmony_ci                    raise RuntimeError(
502e01aa904Sopenharmony_ci                        'Found more than one debuginfo package in '
503e01aa904Sopenharmony_ci                         'this directory. At the moment, fedabipkgdiff '
504e01aa904Sopenharmony_ci                        'is not able to deal with this case. '
505e01aa904Sopenharmony_ci                        'Please create two separate directories and '
506e01aa904Sopenharmony_ci                        'put an RPM and its ancillary debuginfo and '
507e01aa904Sopenharmony_ci                        'devel RPMs in each directory.')
508e01aa904Sopenharmony_ci
509e01aa904Sopenharmony_ci            if selector(local_rpm):
510e01aa904Sopenharmony_ci                rpm_col.add(local_rpm)
511e01aa904Sopenharmony_ci
512e01aa904Sopenharmony_ci        return rpm_col
513e01aa904Sopenharmony_ci
514e01aa904Sopenharmony_ci    def add(self, rpm):
515e01aa904Sopenharmony_ci        """Add a RPM into this collection"""
516e01aa904Sopenharmony_ci        self.rpms.setdefault(rpm.arch, []).append(rpm)
517e01aa904Sopenharmony_ci
518e01aa904Sopenharmony_ci        devel_debuginfo_default = {'debuginfo': None, 'devel': None}
519e01aa904Sopenharmony_ci
520e01aa904Sopenharmony_ci        if rpm.is_debuginfo:
521e01aa904Sopenharmony_ci            self.ancillary_rpms.setdefault(
522e01aa904Sopenharmony_ci                rpm.arch, devel_debuginfo_default)['debuginfo'] = rpm
523e01aa904Sopenharmony_ci
524e01aa904Sopenharmony_ci        if rpm.is_devel:
525e01aa904Sopenharmony_ci            self.ancillary_rpms.setdefault(
526e01aa904Sopenharmony_ci                rpm.arch, devel_debuginfo_default)['devel'] = rpm
527e01aa904Sopenharmony_ci
528e01aa904Sopenharmony_ci    def rpms_iter(self, arches=None, default_behavior=True):
529e01aa904Sopenharmony_ci        """Iterator of RPMs to go through RPMs with specific arches"""
530e01aa904Sopenharmony_ci        arches = sorted(self.rpms.keys())
531e01aa904Sopenharmony_ci
532e01aa904Sopenharmony_ci        for arch in arches:
533e01aa904Sopenharmony_ci            for _rpm in self.rpms[arch]:
534e01aa904Sopenharmony_ci                yield _rpm
535e01aa904Sopenharmony_ci
536e01aa904Sopenharmony_ci    def get_sibling_debuginfo(self, rpm):
537e01aa904Sopenharmony_ci        """Get sibling debuginfo package of given rpm
538e01aa904Sopenharmony_ci
539e01aa904Sopenharmony_ci        The sibling debuginfo is a debug info package for the
540e01aa904Sopenharmony_ci        'rpm'.  Note that if there are several debuginfo packages
541e01aa904Sopenharmony_ci        associated to 'rpm' and users want to get the one which name
542e01aa904Sopenharmony_ci        matches exactly 'rpm', then they might want to use the member
543e01aa904Sopenharmony_ci        function 'get_matching_debuginfo' instead.
544e01aa904Sopenharmony_ci
545e01aa904Sopenharmony_ci        """
546e01aa904Sopenharmony_ci        if rpm.arch not in self.ancillary_rpms:
547e01aa904Sopenharmony_ci            return None
548e01aa904Sopenharmony_ci        return self.ancillary_rpms[rpm.arch].get('debuginfo')
549e01aa904Sopenharmony_ci
550e01aa904Sopenharmony_ci    def get_matching_debuginfo(self, rpm):
551e01aa904Sopenharmony_ci        """Get the debuginfo package that matches a given one """
552e01aa904Sopenharmony_ci        all_debuginfo_list = self.get_all_debuginfo_rpms(rpm)
553e01aa904Sopenharmony_ci        debuginfo_pkg = None
554e01aa904Sopenharmony_ci        for d in all_debuginfo_list:
555e01aa904Sopenharmony_ci            if d.name == '{0}-debuginfo'.format(rpm.name):
556e01aa904Sopenharmony_ci                debuginfo_pkg = d
557e01aa904Sopenharmony_ci                break
558e01aa904Sopenharmony_ci        if not debuginfo_pkg:
559e01aa904Sopenharmony_ci            debuginfo_pkg = self.get_sibling_debuginfo(rpm)
560e01aa904Sopenharmony_ci
561e01aa904Sopenharmony_ci        return debuginfo_pkg
562e01aa904Sopenharmony_ci
563e01aa904Sopenharmony_ci    def get_sibling_devel(self, rpm):
564e01aa904Sopenharmony_ci        """Get sibling devel package of given rpm"""
565e01aa904Sopenharmony_ci        if rpm.arch not in self.ancillary_rpms:
566e01aa904Sopenharmony_ci            return None
567e01aa904Sopenharmony_ci        return self.ancillary_rpms[rpm.arch].get('devel')
568e01aa904Sopenharmony_ci
569e01aa904Sopenharmony_ci    def get_peer_rpm(self, rpm):
570e01aa904Sopenharmony_ci        """Get peer rpm of rpm from this collection"""
571e01aa904Sopenharmony_ci        if rpm.arch not in self.rpms:
572e01aa904Sopenharmony_ci            return None
573e01aa904Sopenharmony_ci        for _rpm in self.rpms[rpm.arch]:
574e01aa904Sopenharmony_ci            if _rpm.is_peer(rpm):
575e01aa904Sopenharmony_ci                return _rpm
576e01aa904Sopenharmony_ci        return None
577e01aa904Sopenharmony_ci
578e01aa904Sopenharmony_ci    def get_all_debuginfo_rpms(self, rpm_info):
579e01aa904Sopenharmony_ci        """Return a list of descriptors of all the debuginfo RPMs associated
580e01aa904Sopenharmony_ci        to a given RPM.
581e01aa904Sopenharmony_ci
582e01aa904Sopenharmony_ci        :param: dict rpm_info a dict representing an RPM.  This was
583e01aa904Sopenharmony_ci        received from the Koji API, either from listRPMs or getRPM.
584e01aa904Sopenharmony_ci        :return: a list of dicts containing RPM descriptors (dicts)
585e01aa904Sopenharmony_ci        for the debuginfo RPMs associated to rpm_info
586e01aa904Sopenharmony_ci        :retype: dict
587e01aa904Sopenharmony_ci        """
588e01aa904Sopenharmony_ci        rpm_infos = self.rpms[rpm_info.arch]
589e01aa904Sopenharmony_ci        result = []
590e01aa904Sopenharmony_ci        for r in rpm_infos:
591e01aa904Sopenharmony_ci            if r.is_debuginfo:
592e01aa904Sopenharmony_ci                result.append(r)
593e01aa904Sopenharmony_ci        return result
594e01aa904Sopenharmony_ci
595e01aa904Sopenharmony_ci
596e01aa904Sopenharmony_cidef generate_comparison_halves(rpm_col1, rpm_col2):
597e01aa904Sopenharmony_ci    """Iterate RPM collection and peer's to generate comparison halves"""
598e01aa904Sopenharmony_ci    for _rpm in rpm_col1.rpms_iter():
599e01aa904Sopenharmony_ci        if _rpm.is_debuginfo:
600e01aa904Sopenharmony_ci            continue
601e01aa904Sopenharmony_ci        if _rpm.is_devel and not global_config.check_all_subpackages:
602e01aa904Sopenharmony_ci            continue
603e01aa904Sopenharmony_ci
604e01aa904Sopenharmony_ci        if global_config.self_compare:
605e01aa904Sopenharmony_ci            rpm2 = _rpm
606e01aa904Sopenharmony_ci        else:
607e01aa904Sopenharmony_ci            rpm2 = rpm_col2.get_peer_rpm(_rpm)
608e01aa904Sopenharmony_ci            if rpm2 is None:
609e01aa904Sopenharmony_ci                logger.warning('Peer RPM of {0} is not found.'.format(_rpm.filename))
610e01aa904Sopenharmony_ci                continue
611e01aa904Sopenharmony_ci
612e01aa904Sopenharmony_ci        debuginfo_list1 = []
613e01aa904Sopenharmony_ci        debuginfo_list2 = []
614e01aa904Sopenharmony_ci
615e01aa904Sopenharmony_ci        # If this is a *devel* package we are looking at, then get all
616e01aa904Sopenharmony_ci        # the debug info packages associated to with the main package
617e01aa904Sopenharmony_ci        # and stick them into the resulting comparison half.
618e01aa904Sopenharmony_ci
619e01aa904Sopenharmony_ci        if _rpm.is_devel:
620e01aa904Sopenharmony_ci            debuginfo_list1 = rpm_col1.get_all_debuginfo_rpms(_rpm)
621e01aa904Sopenharmony_ci        else:
622e01aa904Sopenharmony_ci            debuginfo_list1.append(rpm_col1.get_matching_debuginfo(_rpm))
623e01aa904Sopenharmony_ci
624e01aa904Sopenharmony_ci        devel1 = rpm_col1.get_sibling_devel(_rpm)
625e01aa904Sopenharmony_ci
626e01aa904Sopenharmony_ci        if global_config.self_compare:
627e01aa904Sopenharmony_ci            debuginfo_list2 = debuginfo_list1
628e01aa904Sopenharmony_ci            devel2 = devel1
629e01aa904Sopenharmony_ci        else:
630e01aa904Sopenharmony_ci            if rpm2.is_devel:
631e01aa904Sopenharmony_ci                debuginfo_list2 = rpm_col2.get_all_debuginfo_rpms(rpm2)
632e01aa904Sopenharmony_ci            else:
633e01aa904Sopenharmony_ci                debuginfo_list2.append(rpm_col2.get_matching_debuginfo(rpm2))
634e01aa904Sopenharmony_ci            devel2 = rpm_col2.get_sibling_devel(rpm2)
635e01aa904Sopenharmony_ci
636e01aa904Sopenharmony_ci        yield (ComparisonHalf(subject=_rpm,
637e01aa904Sopenharmony_ci                              ancillary_debug=debuginfo_list1,
638e01aa904Sopenharmony_ci                              ancillary_devel=devel1),
639e01aa904Sopenharmony_ci               ComparisonHalf(subject=rpm2,
640e01aa904Sopenharmony_ci                              ancillary_debug=debuginfo_list2,
641e01aa904Sopenharmony_ci                              ancillary_devel=devel2))
642e01aa904Sopenharmony_ci
643e01aa904Sopenharmony_ci
644e01aa904Sopenharmony_ciclass Brew(object):
645e01aa904Sopenharmony_ci    """Interface to Koji XMLRPC API with enhancements specific to fedabipkgdiff
646e01aa904Sopenharmony_ci
647e01aa904Sopenharmony_ci    kojihub XMLRPC APIs are well-documented in koji's source code. For more
648e01aa904Sopenharmony_ci    details information, please refer to class RootExports within kojihub.py.
649e01aa904Sopenharmony_ci
650e01aa904Sopenharmony_ci    For details of APIs used within fedabipkgdiff, refer to from line
651e01aa904Sopenharmony_ci
652e01aa904Sopenharmony_ci    https://pagure.io/koji/blob/master/f/hub/kojihub.py#_7835
653e01aa904Sopenharmony_ci    """
654e01aa904Sopenharmony_ci
655e01aa904Sopenharmony_ci    def __init__(self, baseurl):
656e01aa904Sopenharmony_ci        """Initialize Brew
657e01aa904Sopenharmony_ci
658e01aa904Sopenharmony_ci        :param str baseurl: the kojihub URL to initialize a session, that is
659e01aa904Sopenharmony_ci        used to access koji XMLRPC APIs.
660e01aa904Sopenharmony_ci        """
661e01aa904Sopenharmony_ci        self.session = koji.ClientSession(baseurl)
662e01aa904Sopenharmony_ci
663e01aa904Sopenharmony_ci    @log_call
664e01aa904Sopenharmony_ci    def listRPMs(self, buildID=None, arches=None, selector=None):
665e01aa904Sopenharmony_ci        """Get list of RPMs of a build from Koji
666e01aa904Sopenharmony_ci
667e01aa904Sopenharmony_ci        Call kojihub.listRPMs to get list of RPMs. Return selected RPMs without
668e01aa904Sopenharmony_ci        changing each RPM information.
669e01aa904Sopenharmony_ci
670e01aa904Sopenharmony_ci        A RPM returned from listRPMs contains following keys:
671e01aa904Sopenharmony_ci
672e01aa904Sopenharmony_ci        - id
673e01aa904Sopenharmony_ci        - name
674e01aa904Sopenharmony_ci        - version
675e01aa904Sopenharmony_ci        - release
676e01aa904Sopenharmony_ci        - nvr (synthesized for sorting purposes)
677e01aa904Sopenharmony_ci        - arch
678e01aa904Sopenharmony_ci        - epoch
679e01aa904Sopenharmony_ci        - payloadhash
680e01aa904Sopenharmony_ci        - size
681e01aa904Sopenharmony_ci        - buildtime
682e01aa904Sopenharmony_ci        - build_id
683e01aa904Sopenharmony_ci        - buildroot_id
684e01aa904Sopenharmony_ci        - external_repo_id
685e01aa904Sopenharmony_ci        - external_repo_name
686e01aa904Sopenharmony_ci        - metadata_only
687e01aa904Sopenharmony_ci        - extra
688e01aa904Sopenharmony_ci
689e01aa904Sopenharmony_ci        :param int buildID: id of a build from which to list RPMs.
690e01aa904Sopenharmony_ci        :param arches: to restrict to list RPMs with specified arches.
691e01aa904Sopenharmony_ci        :type arches: list or tuple
692e01aa904Sopenharmony_ci        :param selector: called to determine if a RPM should be selected and
693e01aa904Sopenharmony_ci        included in the final returned result. Selector must be a callable
694e01aa904Sopenharmony_ci        object and accepts one parameter of a RPM.
695e01aa904Sopenharmony_ci        :type selector: a callable object
696e01aa904Sopenharmony_ci        :return: a list of RPMs, each of them is a dict object
697e01aa904Sopenharmony_ci        :rtype: list
698e01aa904Sopenharmony_ci        """
699e01aa904Sopenharmony_ci        if selector:
700e01aa904Sopenharmony_ci            assert hasattr(selector, '__call__'), 'selector must be callable.'
701e01aa904Sopenharmony_ci        rpms = self.session.listRPMs(buildID=buildID, arches=arches)
702e01aa904Sopenharmony_ci        if selector:
703e01aa904Sopenharmony_ci            rpms = [rpm for rpm in rpms if selector(rpm)]
704e01aa904Sopenharmony_ci        return rpms
705e01aa904Sopenharmony_ci
706e01aa904Sopenharmony_ci    @log_call
707e01aa904Sopenharmony_ci    def getRPM(self, rpminfo):
708e01aa904Sopenharmony_ci        """Get a RPM from koji
709e01aa904Sopenharmony_ci
710e01aa904Sopenharmony_ci        Call kojihub.getRPM, and returns the result directly without any
711e01aa904Sopenharmony_ci        change.
712e01aa904Sopenharmony_ci
713e01aa904Sopenharmony_ci        When not found a RPM, koji.getRPM will return None, then
714e01aa904Sopenharmony_ci        this method will raise RpmNotFound error immediately to claim what is
715e01aa904Sopenharmony_ci        happening. I want to raise fedabipkgdiff specific error rather than
716e01aa904Sopenharmony_ci        koji's GenericError and then raise RpmNotFound again, so I just simply
717e01aa904Sopenharmony_ci        don't use strict parameter to call koji.getRPM.
718e01aa904Sopenharmony_ci
719e01aa904Sopenharmony_ci        :param rpminfo: rpminfo may be a N-V-R.A or a map containing name,
720e01aa904Sopenharmony_ci        version, release, and arch. For example, file-5.25-5.fc24.x86_64, and
721e01aa904Sopenharmony_ci        `{'name': 'file', 'version': '5.25', 'release': '5.fc24', 'arch':
722e01aa904Sopenharmony_ci        'x86_64'}`.
723e01aa904Sopenharmony_ci        :type rpminfo: str or dict
724e01aa904Sopenharmony_ci        :return: a map containing RPM information, that contains same keys as
725e01aa904Sopenharmony_ci        method `Brew.listRPMs`.
726e01aa904Sopenharmony_ci        :rtype: dict
727e01aa904Sopenharmony_ci        :raises RpmNotFound: if a RPM cannot be found with rpminfo.
728e01aa904Sopenharmony_ci        """
729e01aa904Sopenharmony_ci        rpm = self.session.getRPM(rpminfo)
730e01aa904Sopenharmony_ci        if rpm is None:
731e01aa904Sopenharmony_ci            raise RpmNotFound('Cannot find RPM {0}'.format(rpminfo))
732e01aa904Sopenharmony_ci        return rpm
733e01aa904Sopenharmony_ci
734e01aa904Sopenharmony_ci    @log_call
735e01aa904Sopenharmony_ci    def listBuilds(self, packageID, state=None, topone=None,
736e01aa904Sopenharmony_ci                   selector=None, order_by=None, reverse=None):
737e01aa904Sopenharmony_ci        """Get list of builds from Koji
738e01aa904Sopenharmony_ci
739e01aa904Sopenharmony_ci        Call kojihub.listBuilds, and return selected builds without changing
740e01aa904Sopenharmony_ci        each build information.
741e01aa904Sopenharmony_ci
742e01aa904Sopenharmony_ci        By default, only builds with COMPLETE state are queried and returns
743e01aa904Sopenharmony_ci        afterwards.
744e01aa904Sopenharmony_ci
745e01aa904Sopenharmony_ci        :param int packageID: id of package to list builds from.
746e01aa904Sopenharmony_ci        :param int state: build state. There are five states of a build in
747e01aa904Sopenharmony_ci        Koji. fedabipkgdiff only cares about builds with COMPLETE state. If
748e01aa904Sopenharmony_ci        state is omitted, builds with COMPLETE state are queried from Koji by
749e01aa904Sopenharmony_ci        default.
750e01aa904Sopenharmony_ci        :param bool topone: just return the top first build.
751e01aa904Sopenharmony_ci        :param selector: a callable object used to select specific subset of
752e01aa904Sopenharmony_ci        builds. Selector will be called immediately after Koji returns queried
753e01aa904Sopenharmony_ci        builds. When each call to selector, a build is passed to
754e01aa904Sopenharmony_ci        selector. Return True if select current build, False if not.
755e01aa904Sopenharmony_ci        :type selector: a callable object
756e01aa904Sopenharmony_ci        :param str order_by: the attribute name by which to order the builds,
757e01aa904Sopenharmony_ci        for example, name, version, or nvr.
758e01aa904Sopenharmony_ci        :param bool reverse: whether to order builds reversely.
759e01aa904Sopenharmony_ci        :return: a list of builds, even if there is only one build.
760e01aa904Sopenharmony_ci        :rtype: list
761e01aa904Sopenharmony_ci        :raises TypeError: if selector is not callable, or if order_by is not a
762e01aa904Sopenharmony_ci        string value.
763e01aa904Sopenharmony_ci        """
764e01aa904Sopenharmony_ci        if state is None:
765e01aa904Sopenharmony_ci            state = koji.BUILD_STATES['COMPLETE']
766e01aa904Sopenharmony_ci
767e01aa904Sopenharmony_ci        if selector is not None and not hasattr(selector, '__call__'):
768e01aa904Sopenharmony_ci            raise TypeError(
769e01aa904Sopenharmony_ci                '{0} is not a callable object.'.format(str(selector)))
770e01aa904Sopenharmony_ci
771e01aa904Sopenharmony_ci        if order_by is not None and not isinstance(order_by, six.string_types):
772e01aa904Sopenharmony_ci            raise TypeError('order_by {0} is invalid.'.format(order_by))
773e01aa904Sopenharmony_ci
774e01aa904Sopenharmony_ci        builds = self.session.listBuilds(packageID=packageID, state=state)
775e01aa904Sopenharmony_ci        if selector is not None:
776e01aa904Sopenharmony_ci            builds = [build for build in builds if selector(build)]
777e01aa904Sopenharmony_ci        if order_by is not None:
778e01aa904Sopenharmony_ci            # FIXME: is it possible to sort builds by using opts parameter of
779e01aa904Sopenharmony_ci            # listBuilds
780e01aa904Sopenharmony_ci            if order_by == 'nvr':
781e01aa904Sopenharmony_ci                if six.PY2:
782e01aa904Sopenharmony_ci                    builds = sorted(builds, cmp=cmp_nvr, reverse=reverse)
783e01aa904Sopenharmony_ci                else:
784e01aa904Sopenharmony_ci                    builds = sorted(builds,
785e01aa904Sopenharmony_ci                                    key=functools.cmp_to_key(cmp_nvr),
786e01aa904Sopenharmony_ci                                    reverse=reverse)
787e01aa904Sopenharmony_ci            else:
788e01aa904Sopenharmony_ci                builds = sorted(
789e01aa904Sopenharmony_ci                    builds, key=lambda b: b[order_by], reverse=reverse)
790e01aa904Sopenharmony_ci        if topone:
791e01aa904Sopenharmony_ci            builds = builds[0:1]
792e01aa904Sopenharmony_ci
793e01aa904Sopenharmony_ci        return builds
794e01aa904Sopenharmony_ci
795e01aa904Sopenharmony_ci    @log_call
796e01aa904Sopenharmony_ci    def getPackage(self, name):
797e01aa904Sopenharmony_ci        """Get a package from Koji
798e01aa904Sopenharmony_ci
799e01aa904Sopenharmony_ci        :param str name: a package name.
800e01aa904Sopenharmony_ci        :return: a mapping containing package information. For example,
801e01aa904Sopenharmony_ci        `{'id': 1, 'name': 'package'}`.
802e01aa904Sopenharmony_ci        :rtype: dict
803e01aa904Sopenharmony_ci        """
804e01aa904Sopenharmony_ci        package = self.session.getPackage(name)
805e01aa904Sopenharmony_ci        if package is None:
806e01aa904Sopenharmony_ci            package = self.session.getPackage(name.rsplit('-', 1)[0])
807e01aa904Sopenharmony_ci            if package is None:
808e01aa904Sopenharmony_ci                raise KojiPackageNotFound(
809e01aa904Sopenharmony_ci                    'Cannot find package {0}.'.format(name))
810e01aa904Sopenharmony_ci        return package
811e01aa904Sopenharmony_ci
812e01aa904Sopenharmony_ci    @log_call
813e01aa904Sopenharmony_ci    def getBuild(self, buildID):
814e01aa904Sopenharmony_ci        """Get a build from Koji
815e01aa904Sopenharmony_ci
816e01aa904Sopenharmony_ci        Call kojihub.getBuild. Return got build directly without change.
817e01aa904Sopenharmony_ci
818e01aa904Sopenharmony_ci        :param int buildID: id of build to get from Koji.
819e01aa904Sopenharmony_ci        :return: the found build. Return None, if not found a build with
820e01aa904Sopenharmony_ci        buildID.
821e01aa904Sopenharmony_ci        :rtype: dict
822e01aa904Sopenharmony_ci        """
823e01aa904Sopenharmony_ci        return self.session.getBuild(buildID)
824e01aa904Sopenharmony_ci
825e01aa904Sopenharmony_ci    @log_call
826e01aa904Sopenharmony_ci    def get_rpm_build_id(self, name, version, release, arch=None):
827e01aa904Sopenharmony_ci        """Get build ID that contains a RPM with specific nvra
828e01aa904Sopenharmony_ci
829e01aa904Sopenharmony_ci        If arch is not omitted, a RPM can be identified by its N-V-R-A.
830e01aa904Sopenharmony_ci
831e01aa904Sopenharmony_ci        If arch is omitted, name is used to get associated package, and then
832e01aa904Sopenharmony_ci        to get the build.
833e01aa904Sopenharmony_ci
834e01aa904Sopenharmony_ci        Example:
835e01aa904Sopenharmony_ci
836e01aa904Sopenharmony_ci        >>> brew = Brew('url to kojihub')
837e01aa904Sopenharmony_ci        >>> brew.get_rpm_build_id('httpd', '2.4.18', '2.fc24')
838e01aa904Sopenharmony_ci        >>> brew.get_rpm_build_id('httpd', '2.4.18', '2.fc25', 'x86_64')
839e01aa904Sopenharmony_ci
840e01aa904Sopenharmony_ci        :param str name: name of a rpm
841e01aa904Sopenharmony_ci        :param str version: version of a rpm
842e01aa904Sopenharmony_ci        :param str release: release of a rpm
843e01aa904Sopenharmony_ci        :param arch: arch of a rpm
844e01aa904Sopenharmony_ci        :type arch: str or None
845e01aa904Sopenharmony_ci        :return: id of the build from where the RPM is built
846e01aa904Sopenharmony_ci        :rtype: dict
847e01aa904Sopenharmony_ci        :raises KojiPackageNotFound: if name is not found from Koji if arch
848e01aa904Sopenharmony_ci        is None.
849e01aa904Sopenharmony_ci        """
850e01aa904Sopenharmony_ci        if arch is None:
851e01aa904Sopenharmony_ci            package = self.getPackage(name)
852e01aa904Sopenharmony_ci            selector = lambda item: item['version'] == version and \
853e01aa904Sopenharmony_ci                item['release'] == release
854e01aa904Sopenharmony_ci            builds = self.listBuilds(packageID=package['id'],
855e01aa904Sopenharmony_ci                                     selector=selector)
856e01aa904Sopenharmony_ci            if not builds:
857e01aa904Sopenharmony_ci                raise NoBuildsError(
858e01aa904Sopenharmony_ci                    'No builds are selected from package {0}.'.format(
859e01aa904Sopenharmony_ci                        package['name']))
860e01aa904Sopenharmony_ci            return builds[0]['build_id']
861e01aa904Sopenharmony_ci        else:
862e01aa904Sopenharmony_ci            rpm = self.getRPM({'name': name,
863e01aa904Sopenharmony_ci                               'version': version,
864e01aa904Sopenharmony_ci                               'release': release,
865e01aa904Sopenharmony_ci                               'arch': arch,
866e01aa904Sopenharmony_ci                               })
867e01aa904Sopenharmony_ci            return rpm['build_id']
868e01aa904Sopenharmony_ci
869e01aa904Sopenharmony_ci    @log_call
870e01aa904Sopenharmony_ci    def get_package_latest_build(self, package_name, distro):
871e01aa904Sopenharmony_ci        """Get latest build from a package, for a particular distro.
872e01aa904Sopenharmony_ci
873e01aa904Sopenharmony_ci        Example:
874e01aa904Sopenharmony_ci
875e01aa904Sopenharmony_ci        >>> brew = Brew('url to kojihub')
876e01aa904Sopenharmony_ci        >>> brew.get_package_latest_build('httpd', 'fc24')
877e01aa904Sopenharmony_ci
878e01aa904Sopenharmony_ci        :param str package_name: from which package to get the latest build
879e01aa904Sopenharmony_ci        :param str distro: which distro the latest build belongs to
880e01aa904Sopenharmony_ci        :return: the found build
881e01aa904Sopenharmony_ci        :rtype: dict or None
882e01aa904Sopenharmony_ci        :raises NoCompleteBuilds: if there is no latest build of a package.
883e01aa904Sopenharmony_ci        """
884e01aa904Sopenharmony_ci        package = self.getPackage(package_name)
885e01aa904Sopenharmony_ci        selector = lambda item: item['release'].find(distro) > -1
886e01aa904Sopenharmony_ci
887e01aa904Sopenharmony_ci        builds = self.listBuilds(packageID=package['id'],
888e01aa904Sopenharmony_ci                                 selector=selector,
889e01aa904Sopenharmony_ci                                 order_by='nvr',
890e01aa904Sopenharmony_ci                                 reverse=True)
891e01aa904Sopenharmony_ci        if not builds:
892e01aa904Sopenharmony_ci            # So we found no build which distro string exactly matches
893e01aa904Sopenharmony_ci            # the 'distro' parameter.
894e01aa904Sopenharmony_ci            #
895e01aa904Sopenharmony_ci            # Now lets try to get builds which distro string are less
896e01aa904Sopenharmony_ci            # than the value of the 'distro' parameter.  This is for
897e01aa904Sopenharmony_ci            # cases when, for instance, the build of package foo that
898e01aa904Sopenharmony_ci            # is present in current Fedora 27 is foo-1.fc26.  That
899e01aa904Sopenharmony_ci            # build originates from Fedora 26 but is being re-used in
900e01aa904Sopenharmony_ci            # Fedora 27.  So we want this function to pick up that
901e01aa904Sopenharmony_ci            # foo-1.fc26, even though we want the builds of foo that
902e01aa904Sopenharmony_ci            # match the distro string fc27.
903e01aa904Sopenharmony_ci
904e01aa904Sopenharmony_ci            selector = lambda build: get_distro_from_string(build['release']) and \
905e01aa904Sopenharmony_ci                       get_distro_from_string(build['release']) <= distro
906e01aa904Sopenharmony_ci
907e01aa904Sopenharmony_ci            builds = self.listBuilds(packageID=package['id'],
908e01aa904Sopenharmony_ci                                 selector=selector,
909e01aa904Sopenharmony_ci                                 order_by='nvr',
910e01aa904Sopenharmony_ci                                 reverse=True);
911e01aa904Sopenharmony_ci
912e01aa904Sopenharmony_ci        if not builds:
913e01aa904Sopenharmony_ci            raise NoCompleteBuilds(
914e01aa904Sopenharmony_ci                'No complete builds of package {0}'.format(package_name))
915e01aa904Sopenharmony_ci
916e01aa904Sopenharmony_ci        return builds[0]
917e01aa904Sopenharmony_ci
918e01aa904Sopenharmony_ci    @log_call
919e01aa904Sopenharmony_ci    def select_rpms_from_a_build(self, build_id, package_name, arches=None,
920e01aa904Sopenharmony_ci                                 select_subpackages=None):
921e01aa904Sopenharmony_ci        """Select specific RPMs within a build
922e01aa904Sopenharmony_ci
923e01aa904Sopenharmony_ci        RPMs could be filtered be specific criterias by the parameters.
924e01aa904Sopenharmony_ci
925e01aa904Sopenharmony_ci        By default, fedabipkgdiff requires the RPM package, as well as
926e01aa904Sopenharmony_ci        its associated debuginfo and devel packages.  These three
927e01aa904Sopenharmony_ci        packages are selected, and noarch and src are excluded.
928e01aa904Sopenharmony_ci
929e01aa904Sopenharmony_ci        :param int build_id: from which build to select rpms.
930e01aa904Sopenharmony_ci        :param str package_name: which rpm to select that matches this name.
931e01aa904Sopenharmony_ci        :param arches: which arches to select. If arches omits, rpms with all
932e01aa904Sopenharmony_ci        arches except noarch and src will be selected.
933e01aa904Sopenharmony_ci        :type arches: list, tuple or None
934e01aa904Sopenharmony_ci        :param bool select_subpackages: indicate whether to select all RPMs
935e01aa904Sopenharmony_ci        with specific arch from build.
936e01aa904Sopenharmony_ci        :return: a list of RPMs returned from listRPMs
937e01aa904Sopenharmony_ci        :rtype: list
938e01aa904Sopenharmony_ci        """
939e01aa904Sopenharmony_ci        excluded_arches = ('noarch', 'src')
940e01aa904Sopenharmony_ci
941e01aa904Sopenharmony_ci        def rpms_selector(package_name, excluded_arches):
942e01aa904Sopenharmony_ci            return lambda rpm: \
943e01aa904Sopenharmony_ci                rpm['arch'] not in excluded_arches and \
944e01aa904Sopenharmony_ci                (rpm['name'] == package_name or
945e01aa904Sopenharmony_ci                 rpm['name'].endswith('-debuginfo') or
946e01aa904Sopenharmony_ci                 rpm['name'].endswith('-devel'))
947e01aa904Sopenharmony_ci
948e01aa904Sopenharmony_ci        if select_subpackages:
949e01aa904Sopenharmony_ci            selector = lambda rpm: rpm['arch'] not in excluded_arches
950e01aa904Sopenharmony_ci        else:
951e01aa904Sopenharmony_ci            selector = rpms_selector(package_name, excluded_arches)
952e01aa904Sopenharmony_ci        rpm_infos = self.listRPMs(buildID=build_id,
953e01aa904Sopenharmony_ci                                  arches=arches,
954e01aa904Sopenharmony_ci                                  selector=selector)
955e01aa904Sopenharmony_ci        return RPMCollection((RPM(rpm_info) for rpm_info in rpm_infos))
956e01aa904Sopenharmony_ci
957e01aa904Sopenharmony_ci    @log_call
958e01aa904Sopenharmony_ci    def get_latest_built_rpms(self, package_name, distro, arches=None):
959e01aa904Sopenharmony_ci        """Get RPMs from latest build of a package
960e01aa904Sopenharmony_ci
961e01aa904Sopenharmony_ci        :param str package_name: from which package to get the rpms
962e01aa904Sopenharmony_ci        :param str distro: which distro the rpms belong to
963e01aa904Sopenharmony_ci        :param arches: which arches the rpms belong to
964e01aa904Sopenharmony_ci        :type arches: str or None
965e01aa904Sopenharmony_ci        :return: the selected RPMs
966e01aa904Sopenharmony_ci        :rtype: list
967e01aa904Sopenharmony_ci        """
968e01aa904Sopenharmony_ci        latest_build = self.get_package_latest_build(package_name, distro)
969e01aa904Sopenharmony_ci        # Get rpm and debuginfo rpm from each arch
970e01aa904Sopenharmony_ci        return self.select_rpms_from_a_build(latest_build['build_id'],
971e01aa904Sopenharmony_ci                                             package_name,
972e01aa904Sopenharmony_ci                                             arches=arches)
973e01aa904Sopenharmony_ci
974e01aa904Sopenharmony_ci
975e01aa904Sopenharmony_ci@log_call
976e01aa904Sopenharmony_cidef get_session():
977e01aa904Sopenharmony_ci    """Get instance of Brew to talk with Koji"""
978e01aa904Sopenharmony_ci    return Brew(global_config.koji_server)
979e01aa904Sopenharmony_ci
980e01aa904Sopenharmony_ci
981e01aa904Sopenharmony_ci@log_call
982e01aa904Sopenharmony_cidef get_download_dir():
983e01aa904Sopenharmony_ci    """Return the directory holding all downloaded RPMs
984e01aa904Sopenharmony_ci
985e01aa904Sopenharmony_ci    If directory does not exist, it is created automatically.
986e01aa904Sopenharmony_ci
987e01aa904Sopenharmony_ci    :return: path to directory holding downloaded RPMs.
988e01aa904Sopenharmony_ci    :rtype: str
989e01aa904Sopenharmony_ci    """
990e01aa904Sopenharmony_ci    download_dir = os.path.join(HOME_DIR, 'downloads')
991e01aa904Sopenharmony_ci    if not os.path.exists(download_dir):
992e01aa904Sopenharmony_ci        os.makedirs(download_dir)
993e01aa904Sopenharmony_ci    return download_dir
994e01aa904Sopenharmony_ci
995e01aa904Sopenharmony_ci
996e01aa904Sopenharmony_ci@log_call
997e01aa904Sopenharmony_cidef download_rpm(url):
998e01aa904Sopenharmony_ci    """Using curl to download a RPM from Koji
999e01aa904Sopenharmony_ci
1000e01aa904Sopenharmony_ci    Currently, curl is called and runs in a spawned process. pycurl would be a
1001e01aa904Sopenharmony_ci    good way instead. This would be changed in the future.
1002e01aa904Sopenharmony_ci
1003e01aa904Sopenharmony_ci    :param str url: URL of a RPM to download.
1004e01aa904Sopenharmony_ci    :return: True if a RPM is downloaded successfully, False otherwise.
1005e01aa904Sopenharmony_ci    :rtype: bool
1006e01aa904Sopenharmony_ci    """
1007e01aa904Sopenharmony_ci    cmd = 'curl --location --silent {0} -o {1}'.format(
1008e01aa904Sopenharmony_ci        url, os.path.join(get_download_dir(),
1009e01aa904Sopenharmony_ci                          os.path.basename(url)))
1010e01aa904Sopenharmony_ci    if global_config.dry_run:
1011e01aa904Sopenharmony_ci        print('DRY-RUN: {0}'.format(cmd))
1012e01aa904Sopenharmony_ci        return
1013e01aa904Sopenharmony_ci
1014e01aa904Sopenharmony_ci    return_code = subprocess.call(cmd, shell=True)
1015e01aa904Sopenharmony_ci    if return_code > 0:
1016e01aa904Sopenharmony_ci        logger.error('curl fails with returned code: %d.', return_code)
1017e01aa904Sopenharmony_ci        return False
1018e01aa904Sopenharmony_ci    return True
1019e01aa904Sopenharmony_ci
1020e01aa904Sopenharmony_ci
1021e01aa904Sopenharmony_ci@log_call
1022e01aa904Sopenharmony_cidef download_rpms(rpms):
1023e01aa904Sopenharmony_ci    """Download RPMs
1024e01aa904Sopenharmony_ci
1025e01aa904Sopenharmony_ci    :param list rpms: list of RPMs to download.
1026e01aa904Sopenharmony_ci    """
1027e01aa904Sopenharmony_ci    def _download(rpm):
1028e01aa904Sopenharmony_ci        if rpm.is_downloaded:
1029e01aa904Sopenharmony_ci            logger.debug('Reuse %s', rpm.downloaded_file)
1030e01aa904Sopenharmony_ci        else:
1031e01aa904Sopenharmony_ci            logger.debug('Download %s', rpm.download_url)
1032e01aa904Sopenharmony_ci            download_rpm(rpm.download_url)
1033e01aa904Sopenharmony_ci
1034e01aa904Sopenharmony_ci    for rpm in rpms:
1035e01aa904Sopenharmony_ci        _download(rpm)
1036e01aa904Sopenharmony_ci
1037e01aa904Sopenharmony_ci
1038e01aa904Sopenharmony_ci@log_call
1039e01aa904Sopenharmony_cidef build_path_to_abipkgdiff():
1040e01aa904Sopenharmony_ci    """Build the path to the 'abipkgidiff' program to use.
1041e01aa904Sopenharmony_ci
1042e01aa904Sopenharmony_ci    The path to 'abipkgdiff' is either the argument of the
1043e01aa904Sopenharmony_ci    --abipkgdiff command line option, or the path to 'abipkgdiff' as
1044e01aa904Sopenharmony_ci    found in the $PATH environment variable.
1045e01aa904Sopenharmony_ci
1046e01aa904Sopenharmony_ci    :return: str a string representing the path to the 'abipkgdiff'
1047e01aa904Sopenharmony_ci    command.
1048e01aa904Sopenharmony_ci    """
1049e01aa904Sopenharmony_ci    if global_config.abipkgdiff:
1050e01aa904Sopenharmony_ci        return global_config.abipkgdiff
1051e01aa904Sopenharmony_ci    return DEFAULT_ABIPKGDIFF
1052e01aa904Sopenharmony_ci
1053e01aa904Sopenharmony_ci
1054e01aa904Sopenharmony_cidef format_debug_info_pkg_options(option, debuginfo_list):
1055e01aa904Sopenharmony_ci    """Given a list of debug info package descriptors return an option
1056e01aa904Sopenharmony_ci    string that looks like:
1057e01aa904Sopenharmony_ci
1058e01aa904Sopenharmony_ci       option dbg.rpm1 option dbgrpm2 ...
1059e01aa904Sopenharmony_ci
1060e01aa904Sopenharmony_ci    :param: list debuginfo_list a list of instances of the RPM class
1061e01aa904Sopenharmony_ci    representing the debug info rpms to use to construct the option
1062e01aa904Sopenharmony_ci    string.
1063e01aa904Sopenharmony_ci
1064e01aa904Sopenharmony_ci    :return: str a string representing the option string that
1065e01aa904Sopenharmony_ci    concatenate the 'option' parameter before the path to each RPM
1066e01aa904Sopenharmony_ci    contained in 'debuginfo_list'.
1067e01aa904Sopenharmony_ci    """
1068e01aa904Sopenharmony_ci    options = []
1069e01aa904Sopenharmony_ci
1070e01aa904Sopenharmony_ci    for dbg_pkg in debuginfo_list:
1071e01aa904Sopenharmony_ci        if dbg_pkg and dbg_pkg.downloaded_file:
1072e01aa904Sopenharmony_ci            options.append(' {0} {1}'.format(option, dbg_pkg.downloaded_file))
1073e01aa904Sopenharmony_ci
1074e01aa904Sopenharmony_ci    return ' '.join(options) if options else ''
1075e01aa904Sopenharmony_ci
1076e01aa904Sopenharmony_ci@log_call
1077e01aa904Sopenharmony_cidef abipkgdiff(cmp_half1, cmp_half2):
1078e01aa904Sopenharmony_ci    """Run abipkgdiff against found two RPM packages
1079e01aa904Sopenharmony_ci
1080e01aa904Sopenharmony_ci    Construct and execute abipkgdiff to get ABI diff
1081e01aa904Sopenharmony_ci
1082e01aa904Sopenharmony_ci    abipkgdiff \
1083e01aa904Sopenharmony_ci        --d1 package1-debuginfo --d2 package2-debuginfo \
1084e01aa904Sopenharmony_ci        package1-rpm package2-rpm
1085e01aa904Sopenharmony_ci
1086e01aa904Sopenharmony_ci    Output to stdout or stderr from abipkgdiff is not captured. abipkgdiff is
1087e01aa904Sopenharmony_ci    called synchronously. fedabipkgdiff does not return until underlying
1088e01aa904Sopenharmony_ci    abipkgdiff finishes.
1089e01aa904Sopenharmony_ci
1090e01aa904Sopenharmony_ci    :param ComparisonHalf cmp_half1: the first comparison half.
1091e01aa904Sopenharmony_ci    :param ComparisonHalf cmp_half2: the second comparison half.
1092e01aa904Sopenharmony_ci    :return: return code of underlying abipkgdiff execution.
1093e01aa904Sopenharmony_ci    :rtype: int
1094e01aa904Sopenharmony_ci    """
1095e01aa904Sopenharmony_ci    abipkgdiff_tool = build_path_to_abipkgdiff()
1096e01aa904Sopenharmony_ci
1097e01aa904Sopenharmony_ci    suppressions = ''
1098e01aa904Sopenharmony_ci
1099e01aa904Sopenharmony_ci    if global_config.suppr:
1100e01aa904Sopenharmony_ci        suppressions = '--suppressions {0}'.format(global_config.suppr)
1101e01aa904Sopenharmony_ci
1102e01aa904Sopenharmony_ci    if global_config.no_devel_pkg:
1103e01aa904Sopenharmony_ci        devel_pkg1 = ''
1104e01aa904Sopenharmony_ci        devel_pkg2 = ''
1105e01aa904Sopenharmony_ci    else:
1106e01aa904Sopenharmony_ci        if cmp_half1.ancillary_devel is None:
1107e01aa904Sopenharmony_ci            msg = 'Development package for {0} does not exist.'.format(cmp_half1.subject.filename)
1108e01aa904Sopenharmony_ci            if global_config.error_on_warning:
1109e01aa904Sopenharmony_ci                raise RuntimeError(msg)
1110e01aa904Sopenharmony_ci            else:
1111e01aa904Sopenharmony_ci                devel_pkg1 = ''
1112e01aa904Sopenharmony_ci                logger.warning('{0} Ignored.'.format(msg))
1113e01aa904Sopenharmony_ci        else:
1114e01aa904Sopenharmony_ci            devel_pkg1 = '--devel-pkg1 {0}'.format(cmp_half1.ancillary_devel.downloaded_file)
1115e01aa904Sopenharmony_ci
1116e01aa904Sopenharmony_ci        if cmp_half2.ancillary_devel is None:
1117e01aa904Sopenharmony_ci            msg = 'Development package for {0} does not exist.'.format(cmp_half2.subject.filename)
1118e01aa904Sopenharmony_ci            if global_config.error_on_warning:
1119e01aa904Sopenharmony_ci                raise RuntimeError(msg)
1120e01aa904Sopenharmony_ci            else:
1121e01aa904Sopenharmony_ci                devel_pkg2 = ''
1122e01aa904Sopenharmony_ci                logger.warning('{0} Ignored.'.format(msg))
1123e01aa904Sopenharmony_ci        else:
1124e01aa904Sopenharmony_ci            devel_pkg2 = '--devel-pkg2 {0}'.format(cmp_half2.ancillary_devel.downloaded_file)
1125e01aa904Sopenharmony_ci
1126e01aa904Sopenharmony_ci    if cmp_half1.ancillary_debug is None:
1127e01aa904Sopenharmony_ci        msg = 'Debuginfo package for {0} does not exist.'.format(cmp_half1.subject.filename)
1128e01aa904Sopenharmony_ci        if global_config.error_on_warning:
1129e01aa904Sopenharmony_ci            raise RuntimeError(msg)
1130e01aa904Sopenharmony_ci        else:
1131e01aa904Sopenharmony_ci            debuginfo_pkg1 = ''
1132e01aa904Sopenharmony_ci            logger.warning('{0} Ignored.'.format(msg))
1133e01aa904Sopenharmony_ci    else:
1134e01aa904Sopenharmony_ci        debuginfo_pkg1 = format_debug_info_pkg_options("--d1", cmp_half1.ancillary_debug)
1135e01aa904Sopenharmony_ci
1136e01aa904Sopenharmony_ci    if cmp_half2.ancillary_debug is None:
1137e01aa904Sopenharmony_ci        msg = 'Debuginfo package for {0} does not exist.'.format(cmp_half2.subject.filename)
1138e01aa904Sopenharmony_ci        if global_config.error_on_warning:
1139e01aa904Sopenharmony_ci            raise RuntimeError(msg)
1140e01aa904Sopenharmony_ci        else:
1141e01aa904Sopenharmony_ci            debuginfo_pkg2 = ''
1142e01aa904Sopenharmony_ci            logger.warning('{0} Ignored.'.format(msg))
1143e01aa904Sopenharmony_ci    else:
1144e01aa904Sopenharmony_ci        debuginfo_pkg2 = format_debug_info_pkg_options("--d2", cmp_half2.ancillary_debug);
1145e01aa904Sopenharmony_ci
1146e01aa904Sopenharmony_ci    cmd = []
1147e01aa904Sopenharmony_ci
1148e01aa904Sopenharmony_ci    if global_config.self_compare:
1149e01aa904Sopenharmony_ci        cmd = [
1150e01aa904Sopenharmony_ci            abipkgdiff_tool,
1151e01aa904Sopenharmony_ci            '--dso-only' if global_config.dso_only else '',
1152e01aa904Sopenharmony_ci            '--self-check',
1153e01aa904Sopenharmony_ci            debuginfo_pkg1,
1154e01aa904Sopenharmony_ci            cmp_half1.subject.downloaded_file,
1155e01aa904Sopenharmony_ci        ]
1156e01aa904Sopenharmony_ci    else:
1157e01aa904Sopenharmony_ci        cmd = [
1158e01aa904Sopenharmony_ci            abipkgdiff_tool,
1159e01aa904Sopenharmony_ci            suppressions,
1160e01aa904Sopenharmony_ci            '--show-identical-binaries' if global_config.show_identical_binaries else '',
1161e01aa904Sopenharmony_ci            '--no-default-suppression' if global_config.no_default_suppr else '',
1162e01aa904Sopenharmony_ci            '--dso-only' if global_config.dso_only else '',
1163e01aa904Sopenharmony_ci            debuginfo_pkg1,
1164e01aa904Sopenharmony_ci            debuginfo_pkg2,
1165e01aa904Sopenharmony_ci            devel_pkg1,
1166e01aa904Sopenharmony_ci            devel_pkg2,
1167e01aa904Sopenharmony_ci            cmp_half1.subject.downloaded_file,
1168e01aa904Sopenharmony_ci            cmp_half2.subject.downloaded_file,
1169e01aa904Sopenharmony_ci        ]
1170e01aa904Sopenharmony_ci    cmd = [s for s in cmd if s != '']
1171e01aa904Sopenharmony_ci
1172e01aa904Sopenharmony_ci    if global_config.dry_run:
1173e01aa904Sopenharmony_ci        print('DRY-RUN: {0}'.format(' '.join(cmd)))
1174e01aa904Sopenharmony_ci        return
1175e01aa904Sopenharmony_ci
1176e01aa904Sopenharmony_ci    logger.debug('Run: %s', ' '.join(cmd))
1177e01aa904Sopenharmony_ci
1178e01aa904Sopenharmony_ci    print('Comparing the ABI of binaries between {0} and {1}:'.format(
1179e01aa904Sopenharmony_ci        cmp_half1.subject.filename, cmp_half2.subject.filename))
1180e01aa904Sopenharmony_ci    print()
1181e01aa904Sopenharmony_ci
1182e01aa904Sopenharmony_ci    proc = subprocess.Popen(' '.join(cmd), shell=True,
1183e01aa904Sopenharmony_ci                            stdout=subprocess.PIPE, stderr=subprocess.PIPE,
1184e01aa904Sopenharmony_ci                            universal_newlines=True)
1185e01aa904Sopenharmony_ci    # So we could have done: stdout, stderr = proc.communicate()
1186e01aa904Sopenharmony_ci    # But then the documentatin of proc.communicate says:
1187e01aa904Sopenharmony_ci    #
1188e01aa904Sopenharmony_ci    #    Note: The data read is buffered in memory, so do not use this
1189e01aa904Sopenharmony_ci    #    method if the data size is large or unlimited. "
1190e01aa904Sopenharmony_ci    #
1191e01aa904Sopenharmony_ci    # In practice, we are seeing random cases where this
1192e01aa904Sopenharmony_ci    # proc.communicate() function does *NOT* terminate and seems to be
1193e01aa904Sopenharmony_ci    # in a deadlock state.  So we are avoiding it altogether.  We are
1194e01aa904Sopenharmony_ci    # then busy looping, waiting for the spawn process to finish, and
1195e01aa904Sopenharmony_ci    # then we get its output.
1196e01aa904Sopenharmony_ci    #
1197e01aa904Sopenharmony_ci
1198e01aa904Sopenharmony_ci    while True:
1199e01aa904Sopenharmony_ci        if proc.poll() != None:
1200e01aa904Sopenharmony_ci            break
1201e01aa904Sopenharmony_ci
1202e01aa904Sopenharmony_ci    stdout = ''.join(proc.stdout.readlines())
1203e01aa904Sopenharmony_ci    stderr = ''.join(proc.stderr.readlines())
1204e01aa904Sopenharmony_ci
1205e01aa904Sopenharmony_ci    is_ok = proc.returncode == ABIDIFF_OK
1206e01aa904Sopenharmony_ci    is_internal_error = proc.returncode & ABIDIFF_ERROR or proc.returncode & ABIDIFF_USAGE_ERROR
1207e01aa904Sopenharmony_ci    has_abi_change = proc.returncode & ABIDIFF_ABI_CHANGE
1208e01aa904Sopenharmony_ci
1209e01aa904Sopenharmony_ci    if is_internal_error:
1210e01aa904Sopenharmony_ci        six.print_(stderr, file=sys.stderr)
1211e01aa904Sopenharmony_ci    elif is_ok or has_abi_change:
1212e01aa904Sopenharmony_ci        print(stdout)
1213e01aa904Sopenharmony_ci
1214e01aa904Sopenharmony_ci    return proc.returncode
1215e01aa904Sopenharmony_ci
1216e01aa904Sopenharmony_ci
1217e01aa904Sopenharmony_ci@log_call
1218e01aa904Sopenharmony_cidef run_abipkgdiff(rpm_col1, rpm_col2):
1219e01aa904Sopenharmony_ci    """Run abipkgdiff
1220e01aa904Sopenharmony_ci
1221e01aa904Sopenharmony_ci    If one of the executions finds ABI differences, the return code is the
1222e01aa904Sopenharmony_ci    return code from abipkgdiff.
1223e01aa904Sopenharmony_ci
1224e01aa904Sopenharmony_ci    :param RPMCollection rpm_col1: a collection of RPMs
1225e01aa904Sopenharmony_ci    :param RPMCollection rpm_col2: same as rpm_col1
1226e01aa904Sopenharmony_ci    :return: exit code of the last non-zero returned from underlying abipkgdiff
1227e01aa904Sopenharmony_ci    :rtype: int
1228e01aa904Sopenharmony_ci    """
1229e01aa904Sopenharmony_ci    return_codes = [
1230e01aa904Sopenharmony_ci        abipkgdiff(cmp_half1, cmp_half2) for cmp_half1, cmp_half2
1231e01aa904Sopenharmony_ci        in generate_comparison_halves(rpm_col1, rpm_col2)]
1232e01aa904Sopenharmony_ci    return max(return_codes, key=abs) if return_codes else 0
1233e01aa904Sopenharmony_ci
1234e01aa904Sopenharmony_ci
1235e01aa904Sopenharmony_ci@log_call
1236e01aa904Sopenharmony_cidef diff_local_rpm_with_latest_rpm_from_koji():
1237e01aa904Sopenharmony_ci    """Diff against local rpm and remove latest rpm
1238e01aa904Sopenharmony_ci
1239e01aa904Sopenharmony_ci    This operation handles a local rpm and debuginfo rpm and remote ones
1240e01aa904Sopenharmony_ci    located in remote Koji server, that has specific distro specificed by
1241e01aa904Sopenharmony_ci    argument --from.
1242e01aa904Sopenharmony_ci
1243e01aa904Sopenharmony_ci    1/ Suppose the packager has just locally built a package named
1244e01aa904Sopenharmony_ci    foo-3.0.fc24.rpm. To compare the ABI of this locally build package with the
1245e01aa904Sopenharmony_ci    latest stable package from Fedora 23, one would do:
1246e01aa904Sopenharmony_ci
1247e01aa904Sopenharmony_ci    fedabipkgdiff --from fc23 ./foo-3.0.fc24.rpm
1248e01aa904Sopenharmony_ci    """
1249e01aa904Sopenharmony_ci
1250e01aa904Sopenharmony_ci    from_distro = global_config.from_distro
1251e01aa904Sopenharmony_ci    if not is_distro_valid(from_distro):
1252e01aa904Sopenharmony_ci        raise InvalidDistroError('Invalid distro {0}'.format(from_distro))
1253e01aa904Sopenharmony_ci
1254e01aa904Sopenharmony_ci    local_rpm_file = global_config.NVR[0]
1255e01aa904Sopenharmony_ci    if not os.path.exists(local_rpm_file):
1256e01aa904Sopenharmony_ci        raise ValueError('{0} does not exist.'.format(local_rpm_file))
1257e01aa904Sopenharmony_ci
1258e01aa904Sopenharmony_ci    local_rpm = LocalRPM(local_rpm_file)
1259e01aa904Sopenharmony_ci    rpm_col1 = session.get_latest_built_rpms(local_rpm.name,
1260e01aa904Sopenharmony_ci                                             from_distro,
1261e01aa904Sopenharmony_ci                                             arches=local_rpm.arch)
1262e01aa904Sopenharmony_ci    rpm_col2 = RPMCollection.gather_from_dir(local_rpm_file)
1263e01aa904Sopenharmony_ci
1264e01aa904Sopenharmony_ci    if global_config.clean_cache_before:
1265e01aa904Sopenharmony_ci        delete_download_cache()
1266e01aa904Sopenharmony_ci
1267e01aa904Sopenharmony_ci    download_rpms(rpm_col1.rpms_iter())
1268e01aa904Sopenharmony_ci    result = run_abipkgdiff(rpm_col1, rpm_col2)
1269e01aa904Sopenharmony_ci
1270e01aa904Sopenharmony_ci    if global_config.clean_cache_after:
1271e01aa904Sopenharmony_ci        delete_download_cache()
1272e01aa904Sopenharmony_ci
1273e01aa904Sopenharmony_ci    return result
1274e01aa904Sopenharmony_ci
1275e01aa904Sopenharmony_ci
1276e01aa904Sopenharmony_ci@log_call
1277e01aa904Sopenharmony_cidef diff_latest_rpms_based_on_distros():
1278e01aa904Sopenharmony_ci    """abipkgdiff rpms based on two distros
1279e01aa904Sopenharmony_ci
1280e01aa904Sopenharmony_ci    2/ Suppose the packager wants to see how the ABIs of the package foo
1281e01aa904Sopenharmony_ci    evolved between fedora 19 and fedora 22. She would thus type the command:
1282e01aa904Sopenharmony_ci
1283e01aa904Sopenharmony_ci    fedabipkgdiff --from fc19 --to fc22 foo
1284e01aa904Sopenharmony_ci    """
1285e01aa904Sopenharmony_ci
1286e01aa904Sopenharmony_ci    from_distro = global_config.from_distro
1287e01aa904Sopenharmony_ci    to_distro = global_config.to_distro
1288e01aa904Sopenharmony_ci
1289e01aa904Sopenharmony_ci    if not is_distro_valid(from_distro):
1290e01aa904Sopenharmony_ci        raise InvalidDistroError('Invalid distro {0}'.format(from_distro))
1291e01aa904Sopenharmony_ci
1292e01aa904Sopenharmony_ci    if not is_distro_valid(to_distro):
1293e01aa904Sopenharmony_ci        raise InvalidDistroError('Invalid distro {0}'.format(to_distro))
1294e01aa904Sopenharmony_ci
1295e01aa904Sopenharmony_ci    package_name = global_config.NVR[0]
1296e01aa904Sopenharmony_ci
1297e01aa904Sopenharmony_ci    rpm_col1 = session.get_latest_built_rpms(package_name,
1298e01aa904Sopenharmony_ci                                             distro=global_config.from_distro)
1299e01aa904Sopenharmony_ci    rpm_col2 = session.get_latest_built_rpms(package_name,
1300e01aa904Sopenharmony_ci                                             distro=global_config.to_distro)
1301e01aa904Sopenharmony_ci
1302e01aa904Sopenharmony_ci    if global_config.clean_cache_before:
1303e01aa904Sopenharmony_ci        delete_download_cache()
1304e01aa904Sopenharmony_ci
1305e01aa904Sopenharmony_ci    download_rpms(chain(rpm_col1.rpms_iter(), rpm_col2.rpms_iter()))
1306e01aa904Sopenharmony_ci    result = run_abipkgdiff(rpm_col1, rpm_col2)
1307e01aa904Sopenharmony_ci
1308e01aa904Sopenharmony_ci    if global_config.clean_cache_after:
1309e01aa904Sopenharmony_ci        delete_download_cache()
1310e01aa904Sopenharmony_ci
1311e01aa904Sopenharmony_ci    return result
1312e01aa904Sopenharmony_ci
1313e01aa904Sopenharmony_ci
1314e01aa904Sopenharmony_ci@log_call
1315e01aa904Sopenharmony_cidef diff_two_nvras_from_koji():
1316e01aa904Sopenharmony_ci    """Diff two nvras from koji
1317e01aa904Sopenharmony_ci
1318e01aa904Sopenharmony_ci    The arch probably omits, that means febabipkgdiff will diff all arches. If
1319e01aa904Sopenharmony_ci    specificed, the specific arch will be handled.
1320e01aa904Sopenharmony_ci
1321e01aa904Sopenharmony_ci    3/ Suppose the packager wants to compare the ABI of two packages designated
1322e01aa904Sopenharmony_ci    by their name and version. She would issue a command like this:
1323e01aa904Sopenharmony_ci
1324e01aa904Sopenharmony_ci    fedabipkgdiff foo-1.0.fc19 foo-3.0.fc24
1325e01aa904Sopenharmony_ci    fedabipkgdiff foo-1.0.fc19.i686 foo-1.0.fc24.i686
1326e01aa904Sopenharmony_ci    """
1327e01aa904Sopenharmony_ci    left_rpm = koji.parse_NVRA(global_config.NVR[0])
1328e01aa904Sopenharmony_ci    right_rpm = koji.parse_NVRA(global_config.NVR[1])
1329e01aa904Sopenharmony_ci
1330e01aa904Sopenharmony_ci    if is_distro_valid(left_rpm['arch']) and \
1331e01aa904Sopenharmony_ci            is_distro_valid(right_rpm['arch']):
1332e01aa904Sopenharmony_ci        nvr = koji.parse_NVR(global_config.NVR[0])
1333e01aa904Sopenharmony_ci        params1 = (nvr['name'], nvr['version'], nvr['release'], None)
1334e01aa904Sopenharmony_ci
1335e01aa904Sopenharmony_ci        nvr = koji.parse_NVR(global_config.NVR[1])
1336e01aa904Sopenharmony_ci        params2 = (nvr['name'], nvr['version'], nvr['release'], None)
1337e01aa904Sopenharmony_ci    else:
1338e01aa904Sopenharmony_ci        params1 = (left_rpm['name'],
1339e01aa904Sopenharmony_ci                   left_rpm['version'],
1340e01aa904Sopenharmony_ci                   left_rpm['release'],
1341e01aa904Sopenharmony_ci                   left_rpm['arch'])
1342e01aa904Sopenharmony_ci        params2 = (right_rpm['name'],
1343e01aa904Sopenharmony_ci                   right_rpm['version'],
1344e01aa904Sopenharmony_ci                   right_rpm['release'],
1345e01aa904Sopenharmony_ci                   right_rpm['arch'])
1346e01aa904Sopenharmony_ci
1347e01aa904Sopenharmony_ci    build_id = session.get_rpm_build_id(*params1)
1348e01aa904Sopenharmony_ci    rpm_col1 = session.select_rpms_from_a_build(
1349e01aa904Sopenharmony_ci        build_id, params1[0], arches=params1[3],
1350e01aa904Sopenharmony_ci        select_subpackages=global_config.check_all_subpackages)
1351e01aa904Sopenharmony_ci
1352e01aa904Sopenharmony_ci    build_id = session.get_rpm_build_id(*params2)
1353e01aa904Sopenharmony_ci    rpm_col2 = session.select_rpms_from_a_build(
1354e01aa904Sopenharmony_ci        build_id, params2[0], arches=params2[3],
1355e01aa904Sopenharmony_ci        select_subpackages=global_config.check_all_subpackages)
1356e01aa904Sopenharmony_ci
1357e01aa904Sopenharmony_ci    if global_config.clean_cache_before:
1358e01aa904Sopenharmony_ci        delete_download_cache()
1359e01aa904Sopenharmony_ci
1360e01aa904Sopenharmony_ci    download_rpms(chain(rpm_col1.rpms_iter(), rpm_col2.rpms_iter()))
1361e01aa904Sopenharmony_ci    result = run_abipkgdiff(rpm_col1, rpm_col2)
1362e01aa904Sopenharmony_ci
1363e01aa904Sopenharmony_ci    if global_config.clean_cache_after:
1364e01aa904Sopenharmony_ci        delete_download_cache()
1365e01aa904Sopenharmony_ci
1366e01aa904Sopenharmony_ci    return result
1367e01aa904Sopenharmony_ci
1368e01aa904Sopenharmony_ci
1369e01aa904Sopenharmony_ci@log_call
1370e01aa904Sopenharmony_cidef self_compare_rpms_from_distro():
1371e01aa904Sopenharmony_ci    """Compare ABI between same package from a distro
1372e01aa904Sopenharmony_ci
1373e01aa904Sopenharmony_ci    Doing ABI comparison on self package should return no
1374e01aa904Sopenharmony_ci    ABI change and hence return code should be 0. This is useful
1375e01aa904Sopenharmony_ci    to ensure that functionality of libabigail itself
1376e01aa904Sopenharmony_ci    didn't break. This utility can be invoked like this:
1377e01aa904Sopenharmony_ci
1378e01aa904Sopenharmony_ci    fedabipkgdiff --self-compare -a --from fc25 foo
1379e01aa904Sopenharmony_ci    """
1380e01aa904Sopenharmony_ci
1381e01aa904Sopenharmony_ci    from_distro = global_config.from_distro
1382e01aa904Sopenharmony_ci
1383e01aa904Sopenharmony_ci    if not is_distro_valid(from_distro):
1384e01aa904Sopenharmony_ci        raise InvalidDistroError('Invalid distro {0}'.format(from_distro))
1385e01aa904Sopenharmony_ci
1386e01aa904Sopenharmony_ci    package_name = global_config.NVR[0]
1387e01aa904Sopenharmony_ci
1388e01aa904Sopenharmony_ci    rpm_col1 = session.get_latest_built_rpms(package_name,
1389e01aa904Sopenharmony_ci                                             distro=global_config.from_distro)
1390e01aa904Sopenharmony_ci
1391e01aa904Sopenharmony_ci    if global_config.clean_cache_before:
1392e01aa904Sopenharmony_ci        delete_download_cache()
1393e01aa904Sopenharmony_ci
1394e01aa904Sopenharmony_ci    download_rpms(rpm_col1.rpms_iter())
1395e01aa904Sopenharmony_ci    result = run_abipkgdiff(rpm_col1, rpm_col1)
1396e01aa904Sopenharmony_ci
1397e01aa904Sopenharmony_ci    if global_config.clean_cache_after:
1398e01aa904Sopenharmony_ci        delete_download_cache()
1399e01aa904Sopenharmony_ci
1400e01aa904Sopenharmony_ci    return result
1401e01aa904Sopenharmony_ci
1402e01aa904Sopenharmony_ci
1403e01aa904Sopenharmony_ci@log_call
1404e01aa904Sopenharmony_cidef diff_from_two_rpm_files(from_rpm_file, to_rpm_file):
1405e01aa904Sopenharmony_ci    """Diff two RPM files"""
1406e01aa904Sopenharmony_ci    rpm_col1 = RPMCollection.gather_from_dir(from_rpm_file)
1407e01aa904Sopenharmony_ci    rpm_col2 = RPMCollection.gather_from_dir(to_rpm_file)
1408e01aa904Sopenharmony_ci    if global_config.clean_cache_before:
1409e01aa904Sopenharmony_ci        delete_download_cache()
1410e01aa904Sopenharmony_ci    download_rpms(chain(rpm_col1.rpms_iter(), rpm_col2.rpms_iter()))
1411e01aa904Sopenharmony_ci    result = run_abipkgdiff(rpm_col1, rpm_col2)
1412e01aa904Sopenharmony_ci    if global_config.clean_cache_after:
1413e01aa904Sopenharmony_ci        delete_download_cache()
1414e01aa904Sopenharmony_ci    return result
1415e01aa904Sopenharmony_ci
1416e01aa904Sopenharmony_ci
1417e01aa904Sopenharmony_cidef build_commandline_args_parser():
1418e01aa904Sopenharmony_ci    parser = argparse.ArgumentParser(
1419e01aa904Sopenharmony_ci        description='Compare ABI of shared libraries in RPM packages from the '
1420e01aa904Sopenharmony_ci                    'Koji build system')
1421e01aa904Sopenharmony_ci
1422e01aa904Sopenharmony_ci    parser.add_argument(
1423e01aa904Sopenharmony_ci        'NVR',
1424e01aa904Sopenharmony_ci        nargs='*',
1425e01aa904Sopenharmony_ci        help='RPM package N-V-R, N-V-R-A, N, or local RPM '
1426e01aa904Sopenharmony_ci             'file names with relative or absolute path.')
1427e01aa904Sopenharmony_ci    parser.add_argument(
1428e01aa904Sopenharmony_ci        '--dry-run',
1429e01aa904Sopenharmony_ci        required=False,
1430e01aa904Sopenharmony_ci        dest='dry_run',
1431e01aa904Sopenharmony_ci        action='store_true',
1432e01aa904Sopenharmony_ci        help='Don\'t actually do the work. The commands that should be '
1433e01aa904Sopenharmony_ci             'run will be sent to stdout.')
1434e01aa904Sopenharmony_ci    parser.add_argument(
1435e01aa904Sopenharmony_ci        '--from',
1436e01aa904Sopenharmony_ci        required=False,
1437e01aa904Sopenharmony_ci        metavar='DISTRO',
1438e01aa904Sopenharmony_ci        dest='from_distro',
1439e01aa904Sopenharmony_ci        help='baseline Fedora distribution name, for example, fc23')
1440e01aa904Sopenharmony_ci    parser.add_argument(
1441e01aa904Sopenharmony_ci        '--to',
1442e01aa904Sopenharmony_ci        required=False,
1443e01aa904Sopenharmony_ci        metavar='DISTRO',
1444e01aa904Sopenharmony_ci        dest='to_distro',
1445e01aa904Sopenharmony_ci        help='Fedora distribution name to compare against the baseline, for '
1446e01aa904Sopenharmony_ci             'example, fc24')
1447e01aa904Sopenharmony_ci    parser.add_argument(
1448e01aa904Sopenharmony_ci        '-a',
1449e01aa904Sopenharmony_ci        '--all-subpackages',
1450e01aa904Sopenharmony_ci        required=False,
1451e01aa904Sopenharmony_ci        action='store_true',
1452e01aa904Sopenharmony_ci        dest='check_all_subpackages',
1453e01aa904Sopenharmony_ci        help='Check all subpackages instead of only the package specificed in '
1454e01aa904Sopenharmony_ci             'command line.')
1455e01aa904Sopenharmony_ci    parser.add_argument(
1456e01aa904Sopenharmony_ci        '--dso-only',
1457e01aa904Sopenharmony_ci        required=False,
1458e01aa904Sopenharmony_ci        action='store_true',
1459e01aa904Sopenharmony_ci        dest='dso_only',
1460e01aa904Sopenharmony_ci        help='Compare the ABI of shared libraries only. If this option is not '
1461e01aa904Sopenharmony_ci             'provided, the tool compares the ABI of all ELF binaries.')
1462e01aa904Sopenharmony_ci    parser.add_argument(
1463e01aa904Sopenharmony_ci        '--debug',
1464e01aa904Sopenharmony_ci        required=False,
1465e01aa904Sopenharmony_ci        action='store_true',
1466e01aa904Sopenharmony_ci        dest='debug',
1467e01aa904Sopenharmony_ci        help='show debug output')
1468e01aa904Sopenharmony_ci    parser.add_argument(
1469e01aa904Sopenharmony_ci        '--traceback',
1470e01aa904Sopenharmony_ci        required=False,
1471e01aa904Sopenharmony_ci        action='store_true',
1472e01aa904Sopenharmony_ci        dest='show_traceback',
1473e01aa904Sopenharmony_ci        help='show traceback when there is an exception thrown.')
1474e01aa904Sopenharmony_ci    parser.add_argument(
1475e01aa904Sopenharmony_ci        '--server',
1476e01aa904Sopenharmony_ci        required=False,
1477e01aa904Sopenharmony_ci        metavar='URL',
1478e01aa904Sopenharmony_ci        dest='koji_server',
1479e01aa904Sopenharmony_ci        default=DEFAULT_KOJI_SERVER,
1480e01aa904Sopenharmony_ci        help='URL of koji XMLRPC service. Default is {0}'.format(
1481e01aa904Sopenharmony_ci            DEFAULT_KOJI_SERVER))
1482e01aa904Sopenharmony_ci    parser.add_argument(
1483e01aa904Sopenharmony_ci        '--topurl',
1484e01aa904Sopenharmony_ci        required=False,
1485e01aa904Sopenharmony_ci        metavar='URL',
1486e01aa904Sopenharmony_ci        dest='koji_topurl',
1487e01aa904Sopenharmony_ci        default=DEFAULT_KOJI_TOPURL,
1488e01aa904Sopenharmony_ci        help='URL for RPM files access')
1489e01aa904Sopenharmony_ci    parser.add_argument(
1490e01aa904Sopenharmony_ci        '--abipkgdiff',
1491e01aa904Sopenharmony_ci        required=False,
1492e01aa904Sopenharmony_ci        metavar='ABIPKGDIFF',
1493e01aa904Sopenharmony_ci        dest='abipkgdiff',
1494e01aa904Sopenharmony_ci        default='',
1495e01aa904Sopenharmony_ci        help="The path to the 'abipkgtool' command to use. "
1496e01aa904Sopenharmony_ci             "By default use the one found in $PATH.")
1497e01aa904Sopenharmony_ci    parser.add_argument(
1498e01aa904Sopenharmony_ci        '--suppressions',
1499e01aa904Sopenharmony_ci        required=False,
1500e01aa904Sopenharmony_ci        metavar='SUPPR',
1501e01aa904Sopenharmony_ci        dest='suppr',
1502e01aa904Sopenharmony_ci        default='',
1503e01aa904Sopenharmony_ci        help='The suppression specification file to use during comparison')
1504e01aa904Sopenharmony_ci    parser.add_argument(
1505e01aa904Sopenharmony_ci        '--no-default-suppression',
1506e01aa904Sopenharmony_ci        required=False,
1507e01aa904Sopenharmony_ci        action='store_true',
1508e01aa904Sopenharmony_ci        dest='no_default_suppr',
1509e01aa904Sopenharmony_ci        help='Do not load default suppression specifications')
1510e01aa904Sopenharmony_ci    parser.add_argument(
1511e01aa904Sopenharmony_ci        '--no-devel-pkg',
1512e01aa904Sopenharmony_ci        required=False,
1513e01aa904Sopenharmony_ci        action='store_true',
1514e01aa904Sopenharmony_ci        dest='no_devel_pkg',
1515e01aa904Sopenharmony_ci        help='Do not compare ABI with development package')
1516e01aa904Sopenharmony_ci    parser.add_argument(
1517e01aa904Sopenharmony_ci        '--show-identical-binaries',
1518e01aa904Sopenharmony_ci        required=False,
1519e01aa904Sopenharmony_ci        action='store_true',
1520e01aa904Sopenharmony_ci        dest='show_identical_binaries',
1521e01aa904Sopenharmony_ci        help='Show information about binaries whose ABI are identical')
1522e01aa904Sopenharmony_ci    parser.add_argument(
1523e01aa904Sopenharmony_ci        '--error-on-warning',
1524e01aa904Sopenharmony_ci        required=False,
1525e01aa904Sopenharmony_ci        action='store_true',
1526e01aa904Sopenharmony_ci        dest='error_on_warning',
1527e01aa904Sopenharmony_ci        help='Raise error instead of warning')
1528e01aa904Sopenharmony_ci    parser.add_argument(
1529e01aa904Sopenharmony_ci        '--clean-cache',
1530e01aa904Sopenharmony_ci        required=False,
1531e01aa904Sopenharmony_ci        action=SetCleanCacheAction,
1532e01aa904Sopenharmony_ci        dest='clean_cache',
1533e01aa904Sopenharmony_ci        default=None,
1534e01aa904Sopenharmony_ci        help='A convenient way to clean cache without specifying '
1535e01aa904Sopenharmony_ci             '--clean-cache-before and --clean-cache-after at same time')
1536e01aa904Sopenharmony_ci    parser.add_argument(
1537e01aa904Sopenharmony_ci        '--clean-cache-before',
1538e01aa904Sopenharmony_ci        required=False,
1539e01aa904Sopenharmony_ci        action='store_true',
1540e01aa904Sopenharmony_ci        dest='clean_cache_before',
1541e01aa904Sopenharmony_ci        default=None,
1542e01aa904Sopenharmony_ci        help='Clean cache before ABI comparison')
1543e01aa904Sopenharmony_ci    parser.add_argument(
1544e01aa904Sopenharmony_ci        '--clean-cache-after',
1545e01aa904Sopenharmony_ci        required=False,
1546e01aa904Sopenharmony_ci        action='store_true',
1547e01aa904Sopenharmony_ci        dest='clean_cache_after',
1548e01aa904Sopenharmony_ci        default=None,
1549e01aa904Sopenharmony_ci        help='Clean cache after ABI comparison')
1550e01aa904Sopenharmony_ci    parser.add_argument(
1551e01aa904Sopenharmony_ci        '--self-compare',
1552e01aa904Sopenharmony_ci        required=False,
1553e01aa904Sopenharmony_ci        action='store_true',
1554e01aa904Sopenharmony_ci        dest='self_compare',
1555e01aa904Sopenharmony_ci        default=None,
1556e01aa904Sopenharmony_ci        help='ABI comparison on same package')
1557e01aa904Sopenharmony_ci    return parser
1558e01aa904Sopenharmony_ci
1559e01aa904Sopenharmony_ci
1560e01aa904Sopenharmony_cidef main():
1561e01aa904Sopenharmony_ci    parser = build_commandline_args_parser()
1562e01aa904Sopenharmony_ci
1563e01aa904Sopenharmony_ci    args = parser.parse_args()
1564e01aa904Sopenharmony_ci
1565e01aa904Sopenharmony_ci    global global_config
1566e01aa904Sopenharmony_ci    global_config = args
1567e01aa904Sopenharmony_ci
1568e01aa904Sopenharmony_ci    global pathinfo
1569e01aa904Sopenharmony_ci    pathinfo = koji.PathInfo(topdir=global_config.koji_topurl)
1570e01aa904Sopenharmony_ci
1571e01aa904Sopenharmony_ci    global session
1572e01aa904Sopenharmony_ci    session = get_session()
1573e01aa904Sopenharmony_ci
1574e01aa904Sopenharmony_ci    if global_config.debug:
1575e01aa904Sopenharmony_ci        logger.setLevel(logging.DEBUG)
1576e01aa904Sopenharmony_ci
1577e01aa904Sopenharmony_ci    logger.debug(args)
1578e01aa904Sopenharmony_ci
1579e01aa904Sopenharmony_ci    if global_config.from_distro and global_config.self_compare and \
1580e01aa904Sopenharmony_ci            global_config.NVR:
1581e01aa904Sopenharmony_ci        return self_compare_rpms_from_distro()
1582e01aa904Sopenharmony_ci
1583e01aa904Sopenharmony_ci    if global_config.from_distro and global_config.to_distro is None and \
1584e01aa904Sopenharmony_ci            global_config.NVR:
1585e01aa904Sopenharmony_ci        return diff_local_rpm_with_latest_rpm_from_koji()
1586e01aa904Sopenharmony_ci
1587e01aa904Sopenharmony_ci    if global_config.from_distro and global_config.to_distro and \
1588e01aa904Sopenharmony_ci            global_config.NVR:
1589e01aa904Sopenharmony_ci        return diff_latest_rpms_based_on_distros()
1590e01aa904Sopenharmony_ci
1591e01aa904Sopenharmony_ci    if global_config.from_distro is None and global_config.to_distro is None:
1592e01aa904Sopenharmony_ci        if len(global_config.NVR) > 1:
1593e01aa904Sopenharmony_ci            left_one = global_config.NVR[0]
1594e01aa904Sopenharmony_ci            right_one = global_config.NVR[1]
1595e01aa904Sopenharmony_ci
1596e01aa904Sopenharmony_ci            if is_rpm_file(left_one) and is_rpm_file(right_one):
1597e01aa904Sopenharmony_ci                return diff_from_two_rpm_files(left_one, right_one)
1598e01aa904Sopenharmony_ci
1599e01aa904Sopenharmony_ci            both_nvr = match_nvr(left_one) and match_nvr(right_one)
1600e01aa904Sopenharmony_ci            both_nvra = match_nvra(left_one) and match_nvra(right_one)
1601e01aa904Sopenharmony_ci
1602e01aa904Sopenharmony_ci            if both_nvr or both_nvra:
1603e01aa904Sopenharmony_ci                return diff_two_nvras_from_koji()
1604e01aa904Sopenharmony_ci
1605e01aa904Sopenharmony_ci    six.print_('Unknown arguments. Please refer to --help.', file=sys.stderr)
1606e01aa904Sopenharmony_ci    return 1
1607e01aa904Sopenharmony_ci
1608e01aa904Sopenharmony_ci
1609e01aa904Sopenharmony_ciif __name__ == '__main__':
1610e01aa904Sopenharmony_ci    try:
1611e01aa904Sopenharmony_ci        sys.exit(main())
1612e01aa904Sopenharmony_ci    except KeyboardInterrupt:
1613e01aa904Sopenharmony_ci        if global_config is None:
1614e01aa904Sopenharmony_ci            raise
1615e01aa904Sopenharmony_ci        if global_config.debug:
1616e01aa904Sopenharmony_ci            logger.debug('Terminate by user')
1617e01aa904Sopenharmony_ci        else:
1618e01aa904Sopenharmony_ci            six.print_('Terminate by user', file=sys.stderr)
1619e01aa904Sopenharmony_ci        if global_config.show_traceback:
1620e01aa904Sopenharmony_ci            raise
1621e01aa904Sopenharmony_ci        else:
1622e01aa904Sopenharmony_ci            sys.exit(2)
1623e01aa904Sopenharmony_ci    except Exception as e:
1624e01aa904Sopenharmony_ci        if global_config is None:
1625e01aa904Sopenharmony_ci            raise
1626e01aa904Sopenharmony_ci        if global_config.debug:
1627e01aa904Sopenharmony_ci            logger.debug(str(e))
1628e01aa904Sopenharmony_ci        else:
1629e01aa904Sopenharmony_ci            six.print_(str(e), file=sys.stderr)
1630e01aa904Sopenharmony_ci        if global_config.show_traceback:
1631e01aa904Sopenharmony_ci            raise
1632e01aa904Sopenharmony_ci        else:
1633e01aa904Sopenharmony_ci            sys.exit(1)
1634