#!/bin/sh # Check for backwards-incompatible API and ABI changes between two builds # # Backwards-incompatible API changes, such as removing a value from an enum, # are checked by abi-compliance-checker. Such changes can break compilation of # existing programs. # # Backwards-incompatible ABI changes, such as the removal of a public function, # are checked by libigail-tools. Such changes could break run-time dynamic # linking of existing binaries. (We don't use a-c-c for ABI checks because it # needs a debug build, whereas abigail does not.) # # Commands: # --generate : Creates API/ABI dumps in # is expected to be a CMake build result # --check : Compares dumps in and # Comparison exit codes # 0: all good # 1: backwards-incompatible changes found # # Options: # --include-path : a-c-c calls gcc on headers; use this option to add # directories to gcc's search path # # # This script would typically be used before cutting a release: # # 1. Generate API/ABI dump for the old version # # $ check-apiabi-compat.sh --generate libfoo [ libbar ...] # # 2. # # 3. Generate API/ABI dump for the new version # # $ check-apiabi-compat.sh --generate libfoo [ libbar ...] # # 4. Compare the two dumps # # $ check-apiabi-compat.sh --check # # If the check exits 0, all is fine. Otherwise, backwards-incompatible # changes were found, and the librar(ies) need a SOVER bump. set -eu # Preconditions if ! command -v abi-compliance-checker >/dev/null 2>&1; then echo "abi-compliance-checker is not installed." >&2 exit 1 elif ! command -v abidw >/dev/null 2>&1; then echo "abigail-tools are not installed." >&2 exit 1 fi # Some generic functions usage() { echo "Usage: $0 [ --include-path ] --generate libXXX [ libYYY ... ]" >&2 echo " $0 --check " >&2 } get_cmake_project_name() { sed -nr 's/^project\("(.*)".*$/\1/p' CMakeLists.txt } get_cmake_version() { major="$(sed -nr 's/^set\([A-Z]+_VERSION_MAJOR ([0-9]+)\)$/\1/p' CMakeLists.txt)" minor="$(sed -nr 's/^set\([A-Z]+_VERSION_MINOR ([0-9]+)\)$/\1/p' CMakeLists.txt)" patch="$(sed -nr 's/^set\([A-Z]+_VERSION_PATCH ([0-9]+)\)$/\1/p' CMakeLists.txt)" echo "$major.$minor.$patch" } # Option parsing and validation DO_GEN=0 DO_CHECK=0 BUILD_DIR= BUILD_DIR_NEW= INCLUDE_PATHS= while [ "$#" -gt 0 ]; do case "$1" in --generate=*) DO_GEN=1 BUILD_DIR="${1#*=}" shift ;; --generate) DO_GEN=1 if [ -z "${2:-}" ]; then usage exit 1 fi BUILD_DIR="$2" shift 2 ;; --check) DO_CHECK=1 if [ -z "${2:-}" ] || [ -z "${3:-}" ]; then usage exit 1 elif ! [ -d "$2" ]; then echo "$2 is not a directory." >&2 exit 1 elif ! [ -d "$3" ]; then echo "$3 is not a directory." >&2 exit 1 fi BUILD_DIR="$2" BUILD_DIR_NEW="$3" shift 3 ;; --include-path=*) INCLUDE_PATHS="$INCLUDE_PATHS ${1#*=}" shift ;; --include-path) if [ -z "${2:-}" ]; then usage exit 1 fi INCLUDE_PATHS="$INCLUDE_PATHS $2" shift 2 ;; -h | --help) usage exit 1 ;; -?*) usage exit 1 ;; *) break ;; esac done if [ $((DO_GEN + DO_CHECK)) -gt 1 ]; then echo "Can only use one --command." >&2 exit fi PROJECT_NAME="$(get_cmake_project_name)" PROJECT_VERSION="$(get_cmake_version)" LIB_NAMES="" while [ "$#" -gt 0 ]; do if [ "${1#lib}" = "$1" ]; then echo "Library to check must start with libXXX." >&2 exit 1 fi LIB_NAMES="$LIB_NAMES $1" shift done dump_current_api() { echo "Dumping API..." DESCRIPTOR="$BUILD_DIR/apiabi/acc-descriptor.xml" mkdir -p "$BUILD_DIR/apiabi" cat >"$DESCRIPTOR" <$PROJECT_VERSION include $INCLUDE_PATHS EOF # This addresses a bug between a-c-c and universal-ctags, manifested when # a name is use both for a tag and a function name mkdir -p "$BUILD_DIR/apiabi/.ctags.d" echo "--fields=-t" >"$BUILD_DIR/apiabi/.ctags.d/acc.ctags" # Change HOME so that .ctags.d gets picked up by universal-ctags, if used HOME="$BUILD_DIR/apiabi" abi-compliance-checker \ -headers-only \ -lib "$PROJECT_NAME" \ -dump "$DESCRIPTOR" \ -log-path "$BUILD_DIR/apiabi/acc.log" \ -dump-path "$BUILD_DIR/apiabi/api.dump" # acc generates this file with an ancient timestamp, which confuses gzip touch "$BUILD_DIR/apiabi/api.dump" } dump_current_abi() { echo "Dumping ABIs ..." mkdir -p "$BUILD_DIR/apiabi" # The suppressions are needed to avoid including all the internal C++ # symbols, and system types cat >"$BUILD_DIR/apiabi/abidw.suppress" <&2 exit 1 fi fi abidw \ --headers-dir include \ "$abidw_undefined_syms_options" \ --suppressions "$BUILD_DIR/apiabi/abidw.suppress" \ --out-file "${BUILD_DIR}/apiabi/$lib_name.abi.xml" \ "$lib_path" done } # Run the actual commands if [ "$DO_GEN" -eq 1 ]; then dump_current_api dump_current_abi exit 0 elif [ "$DO_CHECK" -eq 1 ]; then # From here on, we don't want to exit on first error set +e abi-compliance-checker \ -strict \ -source \ -library "$PROJECT_NAME" \ -old "$BUILD_DIR/apiabi/api.dump" \ -new "$BUILD_DIR_NEW/apiabi/api.dump" \ -src-report-path "$BUILD_DIR_NEW/apiabi/api_compat_report.html" API_RESULT=$? ABI_RESULT=0 for xml_file in "$BUILD_DIR/apiabi/"lib*.abi.xml; do xml_file_new="$BUILD_DIR_NEW/apiabi/$(basename "$xml_file")" if ! [ -f "$xml_file_new" ]; then echo "Cannot compare, missing file: $xml_file_new" >&2 exit 1 fi abidiff "$xml_file" "$xml_file_new" res=$? [ "$((res & 8))" -ne 0 ] && ABI_RESULT=1 done if [ "$API_RESULT" -gt 0 ]; then echo "ERROR: API changed with possible backwards-compatibility problems." >&2 fi if [ "$ABI_RESULT" -gt 0 ]; then echo "ERROR: ABI changed with possible backwards-compatibility problems." >&2 fi if [ "$((API_RESULT + ABI_RESULT))" -gt 0 ]; then exit 1 fi fi