diff --git a/helpers/check-abi.py b/helpers/check-abi.py index e4c9bf9..56491d4 100755 --- a/helpers/check-abi.py +++ b/helpers/check-abi.py @@ -1,222 +1,222 @@ #!/usr/bin/python3 # check the ABIs against a earlier state. # It is designed to run after create-abi-dump has created the abidump already. # abi-compliance-checker creates a html file with the report. # it can be multiple libraries in one repository (e.g messagelib) # so we have multiple html files for one repository # in order to store them as artifacts in Jenkins add: # # archiveArtifacts artifacts: 'compat_reports/*_compat_reports.html', onlyIfSuccessful: false import os import logging import argparse import decimal import re import subprocess import sys import yaml from helperslib import Packages from helperslib.Version import Version class Library: def __init__(self, packageName, library): self.packageName = packageName self.library = library self.candidates = [] def addCandidate(self, key, entry): entry['packageName'] = key self.candidates.append(entry) def candidate(self): """Find the best candidate to check the ABI against.""" candidate = None timestamp = self.library["timestamp"] if not self.candidates: return None # get a list of tagged candidates released = list(filter(lambda i: i['scmRevision'] in HASH2TAG, self.candidates)) if released: # get the first released version, that is available candidate = min(released, key=lambda i: HASH2TAG[i['scmRevision']]) logging.info("Found tag %s(%s) to check against.", HASH2TAG[candidate['scmRevision']].version, candidate['scmRevision']) else: #TODO: we may want to return None, as the library was never released so far. # get oldest candidate. candidate = min(self.candidates, key=lambda e:e['timestamp']) logging.warning("No released version was found, just use the oldest commit.") # the candidate needs to be older than the current build. if timestamp < candidate['timestamp']: return None return candidate def parseACCOutputToDict(stdout): """Parse output of abi-compliance-checker for further processing and returning a dict. extract binary/source compatibility from acc and calculate a simple bool for the compatibibility. """ checkBlock = re.compile(br"""^Binary compatibility: (?P[0-9.]+)%\s* Source compatibility: (?P[0-9.]+)%\s*$""", re.M) m = checkBlock.search(stdout).groupdict() m['binary'] = decimal.Decimal(m['binary'].decode()) m['source'] = decimal.Decimal(m['source'].decode()) compatibility = m['binary'] == 100 and m['source'] == 100 return { 'binaryCompatibility': float(m['binary']), 'sourceCompatibility': float(m['source']), 'compatibility': compatibility, } # Make sure logging is ready to go logging.basicConfig(level=logging.DEBUG) # Parse the command line arguments we've been given parser = argparse.ArgumentParser(description='Utility to check ABI.') parser.add_argument('--project', type=str, required=True) parser.add_argument('--branchGroup', type=str, required=True) parser.add_argument('--platform', type=str, required=True) parser.add_argument('--environment', type=str, required=True) arguments = parser.parse_args() # Initialize the archive manager ourArchive = Packages.Archive(arguments.environment, 'ABIReference', usingCache = True, contentsSuffix = ".abidump") # Determine which SCM revision we are storing # This will be embedded into the package metadata which might help someone doing some debugging # GIT_COMMIT is set by Jenkins Git plugin, so we can rely on that for most of our builds scmRevision = '' if os.getenv('GIT_COMMIT') != '': scmRevision = os.getenv('GIT_COMMIT') if not scmRevision: scmRevision = subprocess.check_output(["git", "log", "--format=%H", "-n 1", "HEAD"]).strip().decode() # get all tags that are in the current commit tags = subprocess.check_output(["git", "tag", "--merged", scmRevision]).strip().decode().splitlines() # we are not interessed in the commit for annotatated tags itself, we want to know what commit was tagged. commitedTags = [i+"^{}" for i in tags] # resolve tags -> git hashes tagHashes = subprocess.check_output(["git", "rev-parse", *commitedTags]).strip().decode().splitlines() HASH2TAG = {tagHashes[pos]:Version(tag) for pos, tag in enumerate(tags)} # Do we want to check for newer SONAMEs on other buildGroups keepBuildGroup = False if arguments.branchGroup != "kf5-qt5": keepBuildGroup = True # Find all libraries, that are build with the same git commit libraries = [] for key, entry in ourArchive.serverManifest.items(): try: if entry['platform'] != arguments.platform: continue if entry["branchGroup"] != arguments.branchGroup: continue if entry["project"] == arguments.project and entry["scmRevision"] == scmRevision: libraries.append(Library(key,entry)) except KeyError: continue # Find all availabe reference dumps # * same libname # * same SONAME otherwise we have a ABI bump and than it is safe to break ABI for l in libraries: libname = l.library["libname"] soname = l.library["SONAME"] for key, entry in ourArchive.serverManifest.items(): if key == l.packageName: continue if entry['platform'] != arguments.platform: continue # We want to search for the library if entry["libname"] == libname: # only interested, for builds with the same SONAME if entry['SONAME'] == soname: l.addCandidate(key, entry) elif entry['SONAME'] > soname: # Ignore new SONAMEs on other branchGroups. if keepBuildGroup and entry["branchGroup"] != arguments.branchGroup: continue logging.warning("We searched for SONAME = %s, but found a newer SONAME = %s in the builds, that should not happen, as SONAMEs should only rise and never go lower!", soname, entry['SONAME']) # Check every libraries ABI and do not fail, if one is not fine. # Safe the overall retval state retval = 0 # the dictonary that will be written to abi-compatibility-results.yaml resultsYamlFile = {} for l in libraries: library = l.library libname = library['libname'] logging.info("Do an ABI check for %s", libname) candidate = l.candidate() if not candidate: logging.info("Did not found any older build for %s, nothing to check ABI against.",libname) continue # get the packages, we want to test against each other newLibraryPath, _ = ourArchive.retrievePackage(l.packageName) oldLibraryPath, _ = ourArchive.retrievePackage(candidate['packageName']) logging.info("check %s(old) -> %s(new)", candidate['scmRevision'], library['scmRevision']) reportPath = "compat_reports/{libname}_compat_report.html".format(libname=libname) # Basic result yml information yml = { 'reportPath': reportPath, 'ownCommit': scmRevision, 'otherCommit': candidate['scmRevision'], } resultsYamlFile[libname] = yml if candidate['scmRevision'] in HASH2TAG: yml['tag'] = HASH2TAG[candidate['scmRevision']].version # check ABI and write compat reports cmd = [ "abi-compliance-checker", "-report-path", reportPath, "-l", libname, "--old", oldLibraryPath, "--new", newLibraryPath, ] logging.debug(" ".join(cmd)) try: - prog = subprocess.run(cmd, check=True, capture_output=True) + prog = subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) except subprocess.CalledProcessError as e: if e.returncode == 1: # that means that we are not compatible, but still valid output. logging.warning("abi-compliance-checker exited with 1:\n%s", prog.stdout.decode()) yml.update(parseACCOutputToDict(e.stdout)) else: logging.error("abi-compliance-checker exited with %s:\nstdout:\n\ลง%s\nstderr:\n\t%s", e.returncode, e.stdout.decode(), e.stderr.decode()) retval = e.returncode yml['error'] = e.returncode else: logging.debug(prog.stdout.decode()) yml.update(parseACCOutputToDict(prog.stdout)) with open('abi-compatibility-results.yaml', 'w') as f: f.write(yaml.dump(resultsYamlFile, default_flow_style=False)) # We had an issue with one of the ABIs if retval != 0: sys.exit(retval)