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