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