Skip to content
Snippets Groups Projects
apertis-abi-compare 5.83 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.
Currently, apertis-abi-compare only handles single library packages.
"""

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:
Dylan Aïssi's avatar
Dylan Aïssi committed
      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"]
    builddeps = [x for x in builddeps if x not in builddeps_to_remove]
    return builddeps

def define_xml(pkgs_path, version):
Dylan Aïssi's avatar
Dylan Aïssi committed
    xml_descr = list(pathlib.Path(pkgs_path).glob('**/abi-descriptor.xml'))
Dylan Aïssi's avatar
Dylan Aïssi committed
    if xml_descr:
      if len(xml_descr)>1:
        sys.exit('ERROR: several abi-descriptor.xml detected!\n' +
                 'ERROR: apertis-abi-compare only works with single library packages')
      else:
        xml_descr = sorted(xml_descr)[0]
        print(f'INFO: Using XML descriptor: {xml_descr}')
Dylan Aïssi's avatar
Dylan Aïssi committed
    else:
      print('WARNING: XML descriptor not found')
      print('WARNING: Trying to generate an XML descriptor')
Dylan Aïssi's avatar
Dylan Aïssi committed
      xml_descr = os.path.join(pkgs_path,'autogen_xml_descriptor.xml')
Dylan Aïssi's avatar
Dylan Aïssi committed
      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))
Dylan Aïssi's avatar
Dylan Aïssi committed
      generate_xml(xml_descr, version, my_headers, my_libs)
      print(f'INFO: XML descriptor generated: {xml_descr}')
Dylan Aïssi's avatar
Dylan Aïssi committed
      return xml_descr

def generate_xml(xml, version, headers, libs):
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'+'\n'.join(libs)+'\n'
    tree = ET.ElementTree(root)
    ET.indent(tree, space="", level=0)
    tree.write(xml, encoding="utf-8")

def detect_libname(path):
    my_lib = list(pathlib.Path(path).glob('**/*.so'))
    if len(my_lib)>1:
      print('WARNING: several libraries detected!\n' +
            'WARNING: only the first one will be analyzed.')
    else:
      print('INFO: one library detected!')
    my_lib = sorted(my_lib)[0]
Dylan Aïssi's avatar
Dylan Aïssi committed
    my_lib = my_lib.name
    my_lib = my_lib.removeprefix("lib").removesuffix(".so")
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):
Dylan Aïssi's avatar
Dylan Aïssi committed
    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]
    )
Dylan Aïssi's avatar
Dylan Aïssi committed
    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:
Dylan Aïssi's avatar
Dylan Aïssi committed
      sys.exit('ERROR: abi-compliance-checker not found in /usr/bin/\n' +
Dylan Aïssi's avatar
Dylan Aïssi committed
               '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
    file_dsc = list(pathlib.Path(folder_bin).glob('*.dsc'))[0]
    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')
Dylan Aïssi's avatar
Dylan Aïssi committed
    # 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)
Dylan Aïssi's avatar
Dylan Aïssi committed
    lib = detect_libname('pkg_new_extracted')
Dylan Aïssi's avatar
Dylan Aïssi committed
    run_abi_checker(lib, xml_descr_old, my_old_ver, xml_descr_new, my_new_ver)

if __name__ == '__main__':
Dylan Aïssi's avatar
Dylan Aïssi committed
    main()