check-abi.py 7.7 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
#!/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
16 17
import decimal
import re
18 19
import subprocess
import sys
20
import yaml
21 22 23 24 25

from helperslib import Packages
from helperslib.Version import Version

class Library:
26 27 28 29
	def __init__(self, packageName, library):
		self.packageName = packageName
		self.library = library
		self.candidates = []
30

31 32 33
	def addCandidate(self, key, entry):
		entry['packageName'] = key
		self.candidates.append(entry)
34

35 36 37 38
	def candidate(self):
		"""Find the best candidate to check the ABI against."""
		candidate = None
		timestamp = self.library["timestamp"]
39

40 41
		if not self.candidates:
			return None
42

43 44 45 46 47 48 49 50
		# 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.
51

52 53 54
			# get oldest candidate.
			candidate = min(self.candidates, key=lambda e:e['timestamp'])
			logging.warning("No released version was found, just use the oldest commit.")
55

56 57 58
			# the candidate needs to be older than the current build.
			if timestamp < candidate['timestamp']:
				return None
59

60
		return candidate
61 62


63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81
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<binary>[0-9.]+)%\s*
Source compatibility: (?P<source>[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,
	}

82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100
# 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') != '':
101
	scmRevision = os.getenv('GIT_COMMIT')
102 103

if not scmRevision:
104
	scmRevision = subprocess.check_output(["git", "log", "--format=%H", "-n 1", "HEAD"]).strip().decode()
105 106

# get all tags that are in the current commit
107 108 109 110
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]
111 112

# resolve tags -> git hashes
113 114
tagHashes = subprocess.check_output(["git", "rev-parse", *commitedTags]).strip().decode().splitlines()
HASH2TAG = {tagHashes[pos]:Version(tag) for pos, tag in enumerate(tags)}
115 116 117 118 119 120 121 122 123 124 125

# 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:
126 127 128 129
        if entry['platform'] != arguments.platform:
            continue
        if entry["branchGroup"] != arguments.branchGroup:
            continue
130 131 132 133 134 135 136 137 138 139
        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:
140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156
	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'])
157 158 159 160 161

# Check every libraries ABI and do not fail, if one is not fine.
# Safe the overall retval state
retval = 0

162 163 164
# the dictonary that will be written to abi-compatibility-results.yaml
resultsYamlFile = {}

165
for l in libraries:
166 167 168 169 170 171 172 173 174 175 176 177 178 179
	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'])

180 181 182 183 184 185 186 187 188 189 190 191 192
	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

193
	# check ABI and write compat reports
194 195 196
	cmd = [
			"abi-compliance-checker",
			"-report-path", reportPath,
197 198
			"-l", libname,
			"--old", oldLibraryPath,
199 200 201 202
			"--new", newLibraryPath,
	]
	logging.debug(" ".join(cmd))
	try:
203
		prog = subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
204 205
	except subprocess.CalledProcessError as e:
		if e.returncode == 1:    # that means that we are not compatible, but still valid output.
206
			logging.warning("abi-compliance-checker exited with 1:\n%s", e.stdout.decode())
207 208 209

			yml.update(parseACCOutputToDict(e.stdout))
		else:
210
			logging.error("abi-compliance-checker exited with %s:\nstdout:\n\t%s\nstderr:\n\t%s", e.returncode, e.stdout.decode(), e.stderr.decode())
211 212 213 214 215 216 217 218
		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))
219 220 221

# We had an issue with one of the ABIs
if retval != 0:
222
	sys.exit(retval)