diff --git a/abichecker.py b/abichecker.py new file mode 100755 index 00000000..22280165 --- /dev/null +++ b/abichecker.py @@ -0,0 +1,226 @@ +#!/usr/bin/python +# Copyright (c) 2015 SUSE Linux GmbH +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from pprint import pprint, pformat +import os, sys, re +import logging +from optparse import OptionParser +import cmdln +import re +from stat import S_ISREG + +try: + from xml.etree import cElementTree as ET +except ImportError: + import cElementTree as ET + +import osc.conf +import osc.core +import urllib2 +import rpm +from collections import namedtuple + +import ReviewBot + +class ABIChecker(ReviewBot.ReviewBot): + """ check ABI of library packages + """ + + def __init__(self, *args, **kwargs): + ReviewBot.ReviewBot.__init__(self, *args, **kwargs) + + self.ts = rpm.TransactionSet() + self.ts.setVSFlags(rpm._RPMVSF_NOSIGNATURES) + + def check_source_submission(self, src_project, src_package, src_rev, dst_project, dst_package): + ReviewBot.ReviewBot.check_source_submission(self, src_project, src_package, src_rev, dst_project, dst_package) + + if self._get_verifymd5(dst_project, dst_package) is None: + self.logger.info("%s/%s does not exist, skip"%(dst_project, dst_package)) + return None + + # compute list of common repos + myrepos = self.findrepos(src_project, dst_project) + + self.logger.debug(pformat(myrepos)) + + notes = [] + + # fetch cpio headers from source and target + # check file lists for library packages + + missing_debuginfo = set() + so_re = re.compile(r'^(?:/usr)/lib(?:64)?/[^/]+\.so(?:\.[^/]+)') + debugpkg_re = re.compile(r'-debug(?:source|info)(?:-32bit)?$') + for mr in myrepos: + self.logger.debug('scanning %s/%s %s/%s'%(dst_project, dst_package, mr.dstrepo, mr.arch)) + headers = self._fetchcpioheaders(dst_project, dst_package, mr.dstrepo, mr.arch) + lib_packages = dict() # pkgname -> set(lib file names) + pkgs = dict() # pkgname -> rpmhdr + for h in headers: + pkgname = h['name'] + self.logger.debug(pkgname) + pkgs[pkgname] = h + if debugpkg_re.match(pkgname): + continue + for fn, mode in zip(h['filenames'], h['filemodes']): + if so_re.match(fn) and S_ISREG(mode): + self.logger.debug('found lib: %s'%fn) + lib_packages.setdefault(pkgname, set()).add(fn) + + # check whether debug info exists for each lib + for pkgname in sorted(lib_packages.keys()): + # 32bit debug packages have special names + if pkgname.endswith('-32bit'): + dpkgname = pkgname[:-len('-32bit')]+'-debuginfo-32bit' + else: + dpkgname = pkgname+'-debuginfo' + if not dpkgname in pkgs: + missing_debuginfo.add((dst_project, dst_package, mr.dstrepo, mr.arch, pkgname)) + continue + + # check file list of debuginfo package + h = pkgs[dpkgname] + files = set (h['filenames']) + for lib in lib_packages[pkgname]: + fn = '/usr/lib/debug%s.debug'%lib + if not fn in files: + missing_debuginfo.add((dst_project, dst_package, mr.dstrepo, mr.arch, pkgname, lib)) + + if missing_debuginfo: + self.logger.error('missing debuginfo: %s'%pformat(missing_debuginfo)) + return False + + # fetch binary rpms + + # extract binary rpms + + # run abichecker + + # upload result + + def readRpmHeaderFD(self, fd): + h = None + try: + h = self.ts.hdrFromFdno(fd) + except rpm.error, e: + if str(e) == "public key not available": + print str(e) + if str(e) == "public key not trusted": + print str(e) + if str(e) == "error reading package header": + print str(e) + h = None + return h + + def _fetchcpioheaders(self, project, package, repo, arch): + from osc.util.cpio import CpioRead + + u = osc.core.makeurl(self.apiurl, [ 'build', project, repo, arch, package ], + [ 'view=cpioheaders' ]) + r = osc.core.http_GET(u) + from tempfile import NamedTemporaryFile + tmpfile = NamedTemporaryFile(prefix="cpio-", delete=False) + for chunk in r: + tmpfile.write(chunk) + tmpfile.close() + cpio = CpioRead(tmpfile.name) + cpio.read() + for ch in cpio: + # ignore errors + if ch.filename == '.errors': + continue + # the filehandle in the cpio archive is private so + # open it again + with open(tmpfile.name, 'rb') as fh: + fh.seek(ch.dataoff, os.SEEK_SET) + h = self.readRpmHeaderFD(fh) + if h is None: + self.logger.warn("failed to read rpm header for %s"%ch.filename) + else: + yield h + os.unlink(tmpfile.name) + + def findrepos(self, src_project, dst_project): + url = osc.core.makeurl(self.apiurl, ('source', dst_project, '_meta')) + try: + root = ET.parse(osc.core.http_GET(url)).getroot() + except urllib2.HTTPError: + return None + + # build list of target repos as set of name, arch + dstrepos = set() + for repo in root.findall('repository'): + name = repo.attrib['name'] + for node in repo.findall('arch'): + dstrepos.add((name, node.text)) + + url = osc.core.makeurl(self.apiurl, ('source', src_project, '_meta')) + try: + root = ET.parse(osc.core.http_GET(url)).getroot() + except urllib2.HTTPError: + return None + + MR = namedtuple('MatchRepo', ('srcrepo', 'dstrepo', 'arch')) + # set of source repo name, target repo name, arch + matchrepos = set() + for repo in root.findall('repository'): + name = repo.attrib['name'] + path = repo.findall('path') + if path is None or len(path) != 1: + continue + prj = path[0].attrib['project'] + if prj == 'openSUSE:Tumbleweed': + prj = 'openSUSE:Factory' # XXX: hack + if prj != dst_project: + continue + for node in repo.findall('arch'): + arch = node.text + dstname = path[0].attrib['repository'] + if (dstname, arch) in dstrepos: + matchrepos.add(MR(name, dstname, arch)) + + return matchrepos + +class CommandLineInterface(ReviewBot.CommandLineInterface): + + def __init__(self, *args, **kwargs): + ReviewBot.CommandLineInterface.__init__(self, args, kwargs) + + def setup_checker(self): + + apiurl = osc.conf.config['apiurl'] + if apiurl is None: + raise osc.oscerr.ConfigError("missing apiurl") + user = self.options.user + if user is None: + user = osc.conf.get_apiurl_usr(apiurl) + + return ABIChecker(apiurl = apiurl, \ + dryrun = self.options.dry, \ + user = user, \ + logger = self.logger) + +if __name__ == "__main__": + app = CommandLineInterface() + sys.exit( app.main() ) + +# vim: sw=4 et