Newer
Older
#!/usr/bin/env python3
# SPDX-License-Identifier: MPL-2.0
#
# Copyright © 2022 Collabora Ltd
#
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
"""
apertis-abi-compare compares ABI/API between two versions of a package
using abi-compliance-checker and XML descriptor files provided in packages.
If XML descriptor files are not provided, it will try to generate ones based
on the files installed.
apertis-abi-compare takes as input only a path to a folder containing .deb and
a .dsc files. It will download from apt repository the previous version to
compare ABI/API.
"""
import argparse
from debian import deb822, debfile
import os
import pathlib
import re
import subprocess
import sys
def extract_debs(input_folder, output_folder):
list_file = list(pathlib.Path(input_folder).glob('**/*.deb'))
subprocess.run(['/usr/bin/dpkg-deb', '-x',
file, output_folder], check=True)
builddeps = builddeps.split(",")
# Virtual packages not needed
builddeps_to_remove = ["debhelper-compat", "dh-sequence-python3"]
builddeps = [x for x in builddeps if x not in builddeps_to_remove]
return builddeps
xml_descr = list(pathlib.Path(pkgs_path).glob('**/*-abi-descriptor.xml'))
for x in xml_descr:
print(f'INFO: XML descriptor detected: {x}')
xml_data = x.read_text()
xml_data = xml_data.replace('${VER}', version)
xml_data = xml_data.replace('${PATH}', x.parts[0])
x.write_text(xml_data)
print('WARNING: XML descriptor not found')
print('WARNING: Trying to generate an XML descriptor')
my_libs = list(pathlib.Path(pkgs_path).glob('**/*.so'))
my_libs = list(map(str, my_libs))
for idx, my_lib in enumerate(my_libs):
xml_gen = os.path.join(pkgs_path, f'autogen_xml_descriptor_{idx}.xml')
my_headers = list(pathlib.Path(pkgs_path).glob('usr/include/**/*.h'))
my_headers = list(map(str, my_headers))
my_headers = sorted(my_headers)
generate_xml(xml_gen, version, my_headers, my_lib)
print(f'INFO: XML descriptor generated: {xml_gen}')
xml_descr.append(xml_gen)
list_lib_names = detect_libnames(pkgs_path)
return xml_descr, list_lib_names
def generate_xml(xml, version, headers, lib):
root = ET.Element("ABI_Descriptor")
ET.SubElement(root, "version").text = '\n'+version+'\n'
ET.SubElement(root, "headers").text = '\n'+'\n'.join(headers)+'\n'
ET.SubElement(root, "libs").text = '\n'+lib+'\n'
def detect_libnames(path):
my_lib = list(pathlib.Path(path).glob('**/*.so'))
my_lib = [i.name for i in my_lib]
my_lib = [i.removeprefix("lib") for i in my_lib]
my_lib = [i.removesuffix(".so") for i in my_lib]
def run_abi_checker(lib, xml_old, ver_old, xml_new, ver_new):
print('######################################################')
print(f'INFO: Running abi-compliance-checker to compare lib{lib}.so:')
print(f'INFO: - {ver_old}')
print(f'INFO: -- {xml_old}')
print(f'INFO: - {ver_new}')
print(f'INFO: -- {xml_new}')
print('######################################################')
acc_process = subprocess.run(['/usr/bin/abi-compliance-checker', '-lib', lib,
'-old', xml_old, '-v1', ver_old,
'-new', xml_new, '-v2', ver_new])
if acc_process.returncode == 0:
print('INFO: no ABI breakage detected!')
elif acc_process.returncode == 1:
print('WARNING: ABI breakage detected!')
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
return acc_process.returncode
def abi_checker(lib_names_old, xml_descr_old, my_old_ver, lib_names_new, xml_descr_new, my_new_ver):
acc_returncode = 0
for lib in lib_names_new:
my_lib = 'lib'+lib+'.so'
if lib in lib_names_old:
print(f'INFO: {my_lib} detected in both versions')
for xml in xml_descr_new:
xml_data = pathlib.Path(xml).read_text()
if my_lib in xml_data:
xml_new = xml
break
for xml in xml_descr_old:
xml_data = pathlib.Path(xml).read_text()
if my_lib in xml_data:
xml_old = xml
break
rac_returncode = run_abi_checker(lib, xml_old, my_old_ver, xml_new, my_new_ver)
if rac_returncode != 0:
acc_returncode = rac_returncode
else:
print(f'INFO: {my_lib} is not in the old package, no need to run abi-compliance-checker')
return acc_returncode
parser = argparse.ArgumentParser(
description='Check for ABI/API breakage between a new version and a former one')
parser.add_argument('folder_bin', type=str,
help='path to the deb binaries including a .dsc file')
acc_exists = os.path.isfile('/usr/bin/abi-compliance-checker')
sys.exit('ERROR: abi-compliance-checker not found in /usr/bin/\n' +
'ERROR: abi-compliance-checker can be installed with: apt install abi-compliance-checker')
# Extract debs of new version
extract_debs(folder_bin, 'pkg_new_extracted')
# Read dsc of new version to retrieve old bin from apt
file_dsc = list(pathlib.Path(folder_bin).glob('*.dsc'))
if len(file_dsc) != 1:
sys.exit('ERROR: input folder must contain only one .dsc file!\n' +
'ERROR: ' + folder_bin + ' contains ' + str(len(file_dsc)) + ' .dsc file(s).')
my_new_ver = my_dsc['Version']
bin_deb_gen = my_dsc['Binary']
my_builddeps = clean_build_deps(my_dsc['Build-Depends'])
print("INFO: abi-compliance-checker requires having Build-Deps installed")
print("INFO: Trying to install them now:")
print(my_builddeps)
subprocess.run(['sudo', '/usr/bin/apt', 'install'] + my_builddeps)
# Download debs of old version
list_bin_deb = list(bin_deb_gen.split(", "))
folder_old_bin = 'pkg_binaries_old'
os.mkdir(folder_old_bin)
for bin_deb in list_bin_deb:
print(bin_deb)
subprocess.run(['/usr/bin/apt', 'download', bin_deb],
cwd=folder_old_bin)
my_old_deb = list(pathlib.Path(folder_old_bin).glob('*.deb'))[0]
my_old_deb = debfile.DebFile(my_old_deb)
my_old_debcontrol = my_old_deb.debcontrol()
my_old_ver = my_old_debcontrol['Version']
# Extract debs of old version
extract_debs(folder_old_bin, 'pkg_old_extracted')
# Check if xml descriptor is available otherwise generate one
xml_descr_old, lib_names_old = define_xml('pkg_old_extracted', my_old_ver)
xml_descr_new, lib_names_new = define_xml('pkg_new_extracted', my_new_ver)
# Check if library is in both the new and the old packages before running it
acc_returncode = abi_checker(lib_names_old, xml_descr_old, my_old_ver,
lib_names_new, xml_descr_new, my_new_ver)
sys.exit(acc_returncode)