Skip to content
Snippets Groups Projects
apertis-abi-compare 7.63 KiB
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
import xml.etree.ElementTree as ET
def extract_debs(input_folder, output_folder):
Dylan Aïssi's avatar
Dylan Aïssi committed
    list_file = list(pathlib.Path(input_folder).glob('**/*.deb'))
Dylan Aïssi's avatar
Dylan Aïssi committed
    for file in list_file:
        subprocess.run(['/usr/bin/dpkg-deb', '-x',
                        file, output_folder], check=True)


def clean_build_deps(builddeps):
Dylan Aïssi's avatar
Dylan Aïssi committed
    builddeps = re.sub("[\(\[].*?[\)\]]", "", builddeps)
    builddeps = builddeps.replace(" ", "")
Dylan Aïssi's avatar
Dylan Aïssi committed
    builddeps = builddeps.split(",")
    # Virtual packages not needed
    builddeps_to_remove = ["debhelper-compat", "dh-sequence-python3"]
Dylan Aïssi's avatar
Dylan Aïssi committed
    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'))
Dylan Aïssi's avatar
Dylan Aïssi committed
    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)
Dylan Aïssi's avatar
Dylan Aïssi committed
    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):
Dylan Aïssi's avatar
Dylan Aïssi committed
    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")
    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]
Dylan Aïssi's avatar
Dylan Aïssi committed
    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
    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')
Dylan Aïssi's avatar
Dylan Aïssi committed
    args = parser.parse_args()
Dylan Aïssi's avatar
Dylan Aïssi committed
    folder_bin = args.folder_bin
Dylan Aïssi's avatar
Dylan Aïssi committed
    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')
Dylan Aïssi's avatar
Dylan Aïssi committed
    print("Checking for ABI/API breakage!")
Dylan Aïssi's avatar
Dylan Aïssi committed
    # Extract debs of new version
    extract_debs(folder_bin, 'pkg_new_extracted')
Dylan Aïssi's avatar
Dylan Aïssi committed
    # Read dsc of new version to retrieve old bin from apt
Dylan Aïssi's avatar
Dylan Aïssi committed
    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).')
Dylan Aïssi's avatar
Dylan Aïssi committed
    else:
        file_dsc = file_dsc[0]
Dylan Aïssi's avatar
Dylan Aïssi committed
    my_dsc = deb822.Dsc(open(file_dsc))
Dylan Aïssi's avatar
Dylan Aïssi committed
    my_new_ver = my_dsc['Version']
    bin_deb_gen = my_dsc['Binary']
Dylan Aïssi's avatar
Dylan Aïssi committed
    my_builddeps = clean_build_deps(my_dsc['Build-Depends'])
Dylan Aïssi's avatar
Dylan Aïssi committed
    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)
Dylan Aïssi's avatar
Dylan Aïssi committed
    # 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)
Dylan Aïssi's avatar
Dylan Aïssi committed
    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']
Dylan Aïssi's avatar
Dylan Aïssi committed
    # 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)
if __name__ == '__main__':
Dylan Aïssi's avatar
Dylan Aïssi committed
    main()