diff --git a/helpers/check-abi.py b/helpers/check-abi.py index ec71f9c..9a2000f 100755 --- a/helpers/check-abi.py +++ b/helpers/check-abi.py @@ -1,267 +1,307 @@ #!/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 CommonUtils, ToolingSettings 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 __getitem__(self, key): + return self.library[key] + + @property + def reportPath(self): + return "compat_reports/{cmakePackage}_compat_report.html".format(cmakePackage=self['cmakePackage']) + 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']) + candidate['tag'] = HASH2TAG[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 +class ABICompatibilityResults: + """Representing the content of abi-compatibility-results.yaml. + - First add the library via addLibrary + - Afterwards you can extent the directory via __get__ + - At the end, create the fia via write + """ + def __init__(self): + self.dict = {} + + def __getitem__(self, library): + return self.dict[library] + + def addLibrary(self, library): + self.dict[library] = {} + + def write(self): + d = {} + + for library in self.dict: + cantidate = library.candidate() + entry = { + 'reportPath': library.reportPath, + 'ownCommit': library['scmRevision'], + 'otherCommit': candidate['scmRevision'], + } + if 'tag' in candidate: + entry['tag'] = candidate['tag'].version + entry.update(self[library]) + d[library['cmakePackage']] = entry + + with open('abi-compatibility-results.yaml', 'w') as f: + f.write(yaml.dump(d, default_flow_style=False)) + 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") # get acc settings localMetadataPath = os.path.join( CommonUtils.scriptsBaseDirectory(), 'local-metadata', 'abi-compliance-checker.yaml' ) accSettings = ToolingSettings.Loader( arguments.project, arguments.platform ) accSettings.loadSpecificationFile( localMetadataPath ) # 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 = [] def updateAccMetadataVersion(entry): if not 'accMetadataVersion' in entry: updateAccMetadataVersion1(entry) elif entry["accMetadataVersion"] == 2: return def updateAccMetadataVersion1(entry): entry["accMetadataVersion"] = 1 entry["cmakePackage"] = entry["libname"] entry["targets"] = {i:entry["SONAME"] for i in entry["targets"]} +# Find all libraries of current git hash. for key, entry in ourArchive.serverManifest.items(): updateAccMetadataVersion(entry) 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 if not libraries: if accSettings['NoLibrariesFoundFail']: sys.exit("No libraries found and NoLibrariesFoundFail, so we fail hard.") else: logging.info("No libraries found.") sys.exit(0) -# Find all availabe reference dumps +# Find all available reference dumps # * same cmakePackage -# * same SONAME otherwise we have a ABI bump and than it is safe to break ABI +# * same SONAME, otherwise we have a ABI bump and than it is safe to break ABI +for key, entry in ourArchive.serverManifest.items(): + if entry['platform'] != arguments.platform: + continue -for l in libraries: - cmakePackage = l.library["cmakePackage"] - targets = l.library["targets"] - soname = max(targets.values()) - for key, entry in ourArchive.serverManifest.items(): + # Ignore builds on other branches + if keepBuildGroup and entry["branchGroup"] != arguments.branchGroup: + continue + + if entry["project"] != arguments.project: + continue + + if entry["scmRevision"] == scmRevision: + continue + + for l in libraries: + cmakePackage = l.library["cmakePackage"] + targets = l.library["targets"] + soname = max(targets.values()) if key == l.packageName: - continue - if entry['platform'] != arguments.platform: - continue + break # We want to search for the cmakePackage if entry["cmakePackage"] != cmakePackage: continue - # Ignore builds on other branches - if keepBuildGroup and entry["branchGroup"] != arguments.branchGroup: - continue - # TODO: as we may have bundled multiple libraries in one cmakePackage, # we properly need a smater way. if max(entry["targets"].values()) == soname: l.addCandidate(key, entry) - continue + break sameSONAME = False for name, target in targets.items(): try: if entry["targets"][name] == target: sameSONAME = True elif entry["targets"][name] > target: logging.warning("%s: %s has SONAME = %s, but we searched for SONAME %s", entry['scmRevision'], name, entry["targets"][name], target) except KeyError: if entry["accMetadataVersion"] == 2: logging.warning("%s: %s is missing.", entry['scmRevision'], name) if sameSONAME: l.addCandidate(key, entry) # 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 = {} +resultsYamlFile = ABICompatibilityResults() for l in libraries: library = l.library cmakePackage = library['cmakePackage'] logging.info("Do an ABI check for %s", cmakePackage) candidate = l.candidate() if not candidate: logging.info("Did not found any older build for %s, nothing to check ABI against.", cmakePackage) continue + logging.info("check %s(old) -> %s(new)", candidate['scmRevision'], library['scmRevision']) + if 'tag' in candidate: + logging.info("Found tag %s(%s) to check against.", candidate['tag'].version, candidate['scmRevision']) + else: + logging.warning("No released version was found.") + # 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/{cmakePackage}_compat_report.html".format(cmakePackage=cmakePackage) - - # Basic result yml information - yml = { - 'reportPath': reportPath, - 'ownCommit': scmRevision, - 'otherCommit': candidate['scmRevision'], - } - resultsYamlFile[cmakePackage] = yml - if candidate['scmRevision'] in HASH2TAG: - yml['tag'] = HASH2TAG[candidate['scmRevision']].version + # Add library to results yaml file + resultsYamlFile.addLibrary(l) # check ABI and write compat reports cmd = [ "abi-compliance-checker", - "-report-path", reportPath, + "-report-path", l.reportPath, "-l", cmakePackage, "--old", oldLibraryPath, "--new", newLibraryPath, ] logging.debug(" ".join(cmd)) try: 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", e.stdout.decode()) + if e.returncode == 1: # ABI not compatible, but still valid output. + logging.warning("%s\nabi-compliance-checker exited with 1 (not compatible)", e.stdout.decode()) - yml.update(parseACCOutputToDict(e.stdout)) + resultsYamlFile[l].update(parseACCOutputToDict(e.stdout)) else: logging.error("abi-compliance-checker exited with %s:\nstdout:\n\t%s\nstderr:\n\t%s", e.returncode, e.stdout.decode(), e.stderr.decode()) retval = e.returncode - yml['error'] = e.returncode + resultsYamlFile[l]['error'] = e.returncode else: logging.debug(prog.stdout.decode()) - yml.update(parseACCOutputToDict(prog.stdout)) + resultsYamlFile[l].update(parseACCOutputToDict(prog.stdout)) -with open('abi-compatibility-results.yaml', 'w') as f: - f.write(yaml.dump(resultsYamlFile, default_flow_style=False)) +resultsYamlFile.write() # We had an issue with one of the ABIs if retval != 0 and accSettings['checkABIDumpFailHard']: sys.exit("Errors detected and checkABIDumpFailHard is set, so we fail hard.")