#!/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 import xml.etree.ElementTree as ET def extract_debs(input_folder, output_folder): list_file = list(pathlib.Path(input_folder).glob('**/*.deb')) for file in list_file: subprocess.run(['/usr/bin/dpkg-deb', '-x', file, output_folder], check=True) def clean_build_deps(builddeps): builddeps = re.sub("[\(\[].*?[\)\]]", "", builddeps) builddeps = builddeps.replace(" ", "") 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 def define_xml(pkgs_path, version): xml_descr = list(pathlib.Path(pkgs_path).glob('**/*-abi-descriptor.xml')) if xml_descr: 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) else: 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): print(f'INFO: Generating {xml}...') 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' tree = ET.ElementTree(root) ET.indent(tree, space="") tree.write(xml, encoding="utf-8") 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] return 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!') 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 def main(): 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') args = parser.parse_args() folder_bin = args.folder_bin acc_exists = os.path.isfile('/usr/bin/abi-compliance-checker') if not acc_exists: 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') print("Checking for ABI/API breakage!") # 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).') else: file_dsc = file_dsc[0] my_dsc = deb822.Dsc(open(file_dsc)) 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) if __name__ == '__main__': main()