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