Skip to content
Snippets Groups Projects
apertis-abi-compare 5.36 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

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])

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/' +
             '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','-t','unstable','download', bin_deb], cwd=folder_old_bin)
    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()