2
1
mirror of https://github.com/qpdf/qpdf.git synced 2024-12-22 10:58:58 +00:00
qpdf/check_abi
2022-07-24 15:41:02 -04:00

216 lines
7.9 KiB
Python
Executable File

#!/usr/bin/env python3
import os
import sys
import argparse
import subprocess
import re
whoami = os.path.basename(sys.argv[0])
whereami = os.path.dirname(os.path.realpath(__file__))
def warn(*args, **kwargs):
print(*args, file=sys.stderr, **kwargs)
class Main:
def main(self, args=sys.argv[1:], prog=whoami):
options = self.parse_args(args, prog)
if options.action == 'dump':
self.dump(options)
elif options.action == 'check-sizes':
self.check_sizes(options)
elif options.action == 'compare':
self.compare(options)
else:
exit(f'{whoami}: unknown action')
def parse_args(self, args, prog):
parser = argparse.ArgumentParser(
prog=prog,
# formatter_class=argparse.RawDescriptionHelpFormatter,
description='Check ABI for changes',
)
subparsers = parser.add_subparsers(
dest='action',
help='specify subcommand; run action with --help for details',
required=True)
lib_arg = ('--lib', {'help': 'library file', 'required': True})
p_dump = subparsers.add_parser(
'dump',
help='dump qpdf symbols in a library')
p_dump.add_argument(lib_arg[0], **lib_arg[1])
p_check_sizes = subparsers.add_parser(
'check-sizes',
help='check consistency between library and sizes.cc')
p_check_sizes.add_argument(lib_arg[0], **lib_arg[1])
p_compare = subparsers.add_parser(
'compare',
help='compare libraries and sizes')
p_compare.add_argument('--new-lib',
help='new library file',
required=True)
p_compare.add_argument('--old-lib',
help='old library file',
required=True)
p_compare.add_argument('--old-sizes',
help='output of old sizes',
required=True)
p_compare.add_argument('--new-sizes',
help='output of new sizes',
required=True)
return parser.parse_args(args)
def get_versions(self, path):
p = os.path.basename(os.path.realpath(path))
m = re.match(r'^libqpdf.so.(\d+).(\d+).(\d+)$', p)
if not m:
exit(f'{whoami}: {path} does end with libqpdf.so.x.y.z')
major = int(m.group(1))
minor = int(m.group(2))
patch = int(m.group(3))
return (major, minor, patch)
def get_symbols(self, path):
symbols = set()
p = subprocess.run(
['nm', '-D', '--demangle', '--with-symbol-versions', path],
stdout=subprocess.PIPE)
if p.returncode:
exit(f'{whoami}: failed to get symbols from {path}')
for line in p.stdout.decode().split('\n'):
# The LIBQPDF_\d+ comes from the version tag in
# libqpdf.map.in.
m = re.match(r'^[0-9a-f]+ (.) (.+)@@LIBQPDF_\d+\s*$', line)
if not m:
continue
symbols.add(m.group(2))
return symbols
def dump(self, options):
# This is just for manual use to investigate surprises.
for i in sorted(self.get_symbols(options.lib)):
print(i)
def check_sizes(self, options):
# Make sure that every class with methods in the public API
# appears in sizes.cc either explicitly ignored or in a
# print_size call. This enables us to reliably test whether
# any object changed size by following the ABI checking
# procedures outlined in README-maintainer.
# To keep things up to date, whenever we add or remove
# objects, we have to update sizes.cc. The check-sizes option
# can be run at any time on an up-to-date build.
lib = self.get_symbols(options.lib)
classes = set()
for i in sorted(lib):
# Find a symbol that looks like a class method.
m = re.match(
r'(((?:^\S*?::)?(?:[^:\s]+))::([^:\s]+))(?:\[[^\]]+\])?\(',
i)
if m:
full = m.group(1)
clas = m.group(2)
method = m.group(3)
if full.startswith('std::') or method.startswith('~'):
# Sometimes std:: template instantiations make it
# into the library. Ignore those. Also ignore
# classes whose only exported method is a
# destructor.
continue
# Otherwise, if the class exports a method, we
# potentially care about changes to its size, so add
# it.
classes.add(clas)
in_sizes = set()
# Read the sizes.cc to make sure everything's there.
with open(os.path.join(whereami, 'qpdf/sizes.cc'), 'r') as f:
for line in f.readlines():
m = re.search(r'^\s*(?:ignore_class|print_size)\((.*?)\)',
line)
if m:
in_sizes.add(m.group(1))
sizes_only = in_sizes - classes
classes_only = classes - in_sizes
if sizes_only or classes_only:
if sizes_only:
print("classes in sizes.cc but not in the library:")
for i in sorted(sizes_only):
print(' ', i)
if classes_only:
print("classes in the library but not in sizes.cc:")
for i in sorted(classes_only):
print(' ', i)
exit(f'{whoami}: mismatch between library and sizes.cc')
else:
print(f'{whoami}: sizes.cc is consistent with the library')
def read_sizes(self, filename):
sizes = {}
with open(filename, 'r') as f:
for line in f.readlines():
line = line.strip()
m = re.match(r'^(.*) (\d+)$', line)
if not m:
exit(f'{filename}: bad sizes line: {line}')
sizes[m.group(1)] = m.group(2)
return sizes
def compare(self, options):
old_version = self.get_versions(options.old_lib)
new_version = self.get_versions(options.new_lib)
old = self.get_symbols(options.old_lib)
new = self.get_symbols(options.new_lib)
if old_version > new_version:
exit(f'{whoami}: old version is newer than new version')
allow_abi_change = new_version[0] > old_version[0]
allow_added = allow_abi_change or (new_version[1] > old_version[1])
removed = sorted(old - new)
added = sorted(new - old)
if removed:
print('INTERFACES REMOVED:')
for i in removed:
print(' ', i)
else:
print('No interfaces were removed')
if added:
print('INTERFACES ADDED')
for i in added:
print(' ', i)
else:
print('No interfaces were added')
if removed and not allow_abi_change:
exit(f'{whoami}: **ERROR**: major version must be bumped')
elif added and not allow_added:
exit(f'{whoami}: **ERROR**: minor version must be bumped')
else:
print(f'{whoami}: ABI check passed.')
old_sizes = self.read_sizes(options.old_sizes)
new_sizes = self.read_sizes(options.new_sizes)
size_errors = False
for k, v in old_sizes.items():
if k in new_sizes and v != new_sizes[k]:
size_errors = True
print(f'{k} changed size from {v} to {new_sizes[k]}')
if size_errors:
if not allow_abi_change:
exit(f'{whoami}:'
'size changes detected; this is an ABI change.')
else:
print(f'{whoami}: no size changes detected')
if __name__ == '__main__':
try:
Main().main()
except KeyboardInterrupt:
exit(130)