#!/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 = os.listdir(input_folder) for file in list_file: if file.endswith(".deb"): file = os.path.join(input_folder,file) subprocess.run(['/usr/bin/dpkg-deb', '-x', file, output_folder], check=True) def clean_build_deps(builddeps): builddeps = re.sub("[\(\[].*?[\)\]]", "", builddeps) builddeps = re.sub(" ", "", builddeps) 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): xml_descr = xml_descr[0] print(f'INFO: Using XML descriptor: {xml_descr}') else: print('WARNING: XML descriptor not found') print('WARNING: Trying to generate an XML descriptor') xml_descr = os.path.join(pkgs_path,'autogen_xml_descriptor.xml') my_headers = list(pathlib.Path(pkgs_path).glob('usr/include/**/*.h')) my_headers = list(map(str, my_headers)) my_libs = list(pathlib.Path(pkgs_path).glob('**/*.so')) my_libs = list(map(str, my_libs)) generate_xml(xml_descr, version, my_headers, my_libs) print(f'INFO: XML descriptor generated: {xml_descr}') return xml_descr def generate_xml(xml, version, headers, libs): print(f'INFO: Generating {xml}...') f = open(xml, "w") f.write("<version>\n") f.write(" " + version + "\n") f.write("</version>\n") f.write("<headers>\n") for h in headers: f.write(" " + h + "\n") f.write("</headers>\n") f.write("<libs>\n") for l in libs: f.write(" " + l + "\n") f.write("</libs>\n") f.close() def detect_libname(path): my_lib = list(pathlib.Path(path).glob('**/*.so'))[0] my_lib = my_lib.name my_lib = re.sub("lib", "", my_lib) my_lib = re.sub(".so", "", my_lib) return my_lib def run_abi_checker(lib, xml_old, ver_old, xml_new, ver_new): print(f'INFO: Running abi-compliance-checker to compare {lib}:') print(f'INFO: - {ver_old} {xml_old}') print(f'INFO: - {ver_new} {xml_new}') subprocess.run(['/usr/bin/abi-compliance-checker', '-lib', lib, '-old', xml_old, '-v1', ver_old, '-new', xml_new, '-v2', ver_new] ) def main(): parser = argparse.ArgumentParser(description='Check for ABI/API breakage between a new version and a former one') parser.add_argument('--binaries', dest='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') if(not bool(folder_bin)): sys.exit('ERROR: --binaries is not defined') 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'))[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 in xml descriptor is available otherwise generate one xml_descr_old = define_xml('pkg_old_extracted', my_old_ver) xml_descr_new = define_xml('pkg_new_extracted', my_new_ver) lib = detect_libname('pkg_new_extracted') run_abi_checker(lib, xml_descr_old, my_old_ver, xml_descr_new, my_new_ver) if __name__ == '__main__': main()