diff --git a/.github/workflows/make-release.yml b/.github/workflows/make-release.yml index 6644a80ccc..d1c6dca5db 100644 --- a/.github/workflows/make-release.yml +++ b/.github/workflows/make-release.yml @@ -13,6 +13,16 @@ on: required: true type: boolean default: true + skip_apiabi_check: + description: 'Skip API/ABI compatibility check' + required: false + type: boolean + default: false + apiabi_compare_tag: + description: 'Tag to compare against for API/ABI check (default: latest release)' + required: false + type: string + default: '' env: GH_TOKEN: ${{ github.token }} @@ -33,12 +43,18 @@ jobs: ref: ${{ inputs.commit != '' && inputs.commit || github.ref_name }} fetch-depth: 0 + - name: Install API/ABI check tools + if: ${{ github.event.inputs.skip_apiabi_check != 'true' }} + run: sudo apt-get install -y abi-compliance-checker abigail-tools + - name: Run release checks id: checks run: bash scripts/make-release-checks.sh ${{ github.event.inputs.dry_run == 'true' && '--dry-run' || '' }} env: GITHUB_REPOSITORY: ${{ github.repository }} RELEASE_BRANCH: ${{ github.ref_name }} + SKIP_APIABI_CHECK: ${{ github.event.inputs.skip_apiabi_check }} + APIABI_COMPARE_TAG: ${{ github.event.inputs.apiabi_compare_tag }} - name: Create release tag if: ${{ github.event.inputs.dry_run == 'false' }} diff --git a/scripts/check-apiabi-compat.sh b/scripts/check-apiabi-compat.sh index 078abb864f..8075aaf9f1 100755 --- a/scripts/check-apiabi-compat.sh +++ b/scripts/check-apiabi-compat.sh @@ -55,7 +55,9 @@ fi # Some generic functions usage() { echo "Usage: $0 [ --include-path ] --generate libXXX [ libYYY ... ]" >&2 - echo " $0 --check " >&2 + echo " $0 [ --strict ] --check " >&2 + echo "" >&2 + echo " --strict: fail on any API/ABI change, including backwards-compatible additions" >&2 } get_cmake_project_name() { @@ -72,6 +74,7 @@ get_cmake_version() { # Option parsing and validation DO_GEN=0 DO_CHECK=0 +STRICT=0 BUILD_DIR= BUILD_DIR_NEW= INCLUDE_PATHS= @@ -119,6 +122,10 @@ while [ "$#" -gt 0 ]; do INCLUDE_PATHS="$INCLUDE_PATHS $2" shift 2 ;; + --strict) + STRICT=1 + shift + ;; -h | --help) usage exit 1 @@ -258,13 +265,24 @@ elif [ "$DO_CHECK" -eq 1 ]; then abidiff "$xml_file" "$xml_file_new" res=$? [ "$((res & 8))" -ne 0 ] && ABI_RESULT=1 + # check bit 2 for compatible ABI changes (like new symbols) and if + # STRICT is set then handle this as an error. + [ "$STRICT" -eq 1 ] && [ "$((res & 4))" -ne 0 ] && ABI_RESULT=1 done if [ "$API_RESULT" -gt 0 ]; then - echo "ERROR: API changed with possible backwards-compatibility problems." >&2 + if [ "$STRICT" -eq 1 ]; then + echo "ERROR: API changed — a minor version bump is required." >&2 + else + echo "ERROR: API changed with backwards-incompatible problems — a major version bump is required." >&2 + fi fi if [ "$ABI_RESULT" -gt 0 ]; then - echo "ERROR: ABI changed with possible backwards-compatibility problems." >&2 + if [ "$STRICT" -eq 1 ]; then + echo "ERROR: ABI changed — a minor version bump is required." >&2 + else + echo "ERROR: ABI changed with backwards-incompatible problems — a major version bump is required." >&2 + fi fi if [ "$((API_RESULT + ABI_RESULT))" -gt 0 ]; then exit 1 diff --git a/scripts/check-release-apiabi.sh b/scripts/check-release-apiabi.sh new file mode 100755 index 0000000000..d31d240418 --- /dev/null +++ b/scripts/check-release-apiabi.sh @@ -0,0 +1,131 @@ +#!/bin/bash +# Check API/ABI compatibility between the previous release tag and current HEAD. +# +# Finds the most recent vX.Y.Z tag, checks it out in a temporary git worktree, +# builds both versions with shared libs enabled, and uses check-apiabi-compat.sh +# to compare the results. +# +# Exit codes: +# 0: compatible, or check was skipped +# 1: backwards-incompatible changes found, or build failed +# +# Options: +# --tag : compare against this tag instead of the latest release +# +# Environment: +# SKIP_APIABI_CHECK: set to 1 or true to skip +# APIABI_COMPARE_TAG: equivalent to --tag (used by CI) + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" + +usage() { + echo "Usage: $0 [--tag ]" >&2 + echo " --tag Compare against this release tag (default: latest)" >&2 +} + +COMPARE_TAG="${APIABI_COMPARE_TAG:-}" +while [[ "$#" -gt 0 ]]; do + case "$1" in + --tag) + if [[ -z "${2:-}" ]]; then usage; exit 1; fi + COMPARE_TAG="$2" + shift 2 + ;; + --tag=*) + COMPARE_TAG="${1#*=}" + shift + ;; + -h | --help) + usage; exit 0 + ;; + *) + usage; exit 1 + ;; + esac +done + +if [[ "${SKIP_APIABI_CHECK:-}" == "1" || "${SKIP_APIABI_CHECK:-}" == "true" ]]; then + echo "SKIP_APIABI_CHECK is set - skipping API/ABI compatibility check" + exit 0 +fi + +if ! command -v abi-compliance-checker >/dev/null 2>&1 || ! command -v abidw >/dev/null 2>&1; then + echo "Warning: abi-compliance-checker or abigail-tools not installed - skipping API/ABI check" + exit 0 +fi + +discover_libs() { + local build_dir="$1" + local libs=() + for dir in "$build_dir/src" "$build_dir/bin"; do + [[ -d "$dir" ]] || continue + for f in "$dir"/lib*.so; do + [[ -f "$f" ]] && libs+=("$(basename "$f" .so)") + done + done + echo "${libs[@]}" +} + +if [[ -n "${COMPARE_TAG}" ]]; then + PREV_TAG="${COMPARE_TAG}" + if ! git -C "$REPO_ROOT" rev-parse --verify "${PREV_TAG}^{}" >/dev/null 2>&1; then + echo "Error: tag '${PREV_TAG}' not found in repository." >&2 + exit 1 + fi +else + PREV_TAG=$(git -C "$REPO_ROOT" tag --sort=-v:refname | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | head -n 1 || true) + if [[ -z "${PREV_TAG}" ]]; then + echo "Warning: no previous release tag found - skipping API/ABI check" + exit 0 + fi +fi +OLD_VERSION="${PREV_TAG#v}" +OLD_MAJOR="${OLD_VERSION%%.*}" +OLD_MINOR="${OLD_VERSION#*.}"; OLD_MINOR="${OLD_MINOR%%.*}" + +NEW_MAJOR=$(grep "set(LLAMA_VERSION_MAJOR" "$REPO_ROOT/CMakeLists.txt" | sed 's/.*MAJOR \([0-9]*\).*/\1/') +NEW_MINOR=$(grep "set(LLAMA_VERSION_MINOR" "$REPO_ROOT/CMakeLists.txt" | sed 's/.*MINOR \([0-9]*\).*/\1/') + +if [[ "$NEW_MAJOR" -gt "$OLD_MAJOR" ]]; then + echo "Major version increment ($OLD_MAJOR -> $NEW_MAJOR): API/ABI breaking changes are expected, skipping compatibility check." + exit 0 +fi + +CHECK_FLAGS=() +if [[ "$NEW_MINOR" -eq "$OLD_MINOR" ]]; then + echo "Patch version bump detected: checking for any API/ABI changes (a minor bump is required if any are found)..." + CHECK_FLAGS+=(--strict) +else + echo "Minor version bump detected: checking for backwards-incompatible API/ABI changes..." +fi + +echo "Checking API/ABI compatibility against ${PREV_TAG}..." + +WORKTREE_DIR=$(mktemp -d) +BUILD_OLD=$(mktemp -d) +BUILD_NEW=$(mktemp -d) + +cleanup() { + git -C "$REPO_ROOT" worktree remove --force "$WORKTREE_DIR" 2>/dev/null || true + rm -rf "$WORKTREE_DIR" "$BUILD_OLD" "$BUILD_NEW" +} +trap cleanup EXIT + +git -C "$REPO_ROOT" worktree add "$WORKTREE_DIR" "$PREV_TAG" + +cmake -S "$WORKTREE_DIR" -B "$BUILD_OLD" -DBUILD_SHARED_LIBS=ON -DCMAKE_BUILD_TYPE=RelWithDebInfo +cmake --build "$BUILD_OLD" --parallel "$(nproc)" +OLD_LIBS=($(discover_libs "$BUILD_OLD")) +echo "Libraries found in old build: ${OLD_LIBS[*]}" + +cmake -S "$REPO_ROOT" -B "$BUILD_NEW" -DBUILD_SHARED_LIBS=ON -DCMAKE_BUILD_TYPE=RelWithDebInfo +cmake --build "$BUILD_NEW" --parallel "$(nproc)" +NEW_LIBS=($(discover_libs "$BUILD_NEW")) +echo "Libraries found in new build: ${NEW_LIBS[*]}" + +(cd "$WORKTREE_DIR" && "$SCRIPT_DIR/check-apiabi-compat.sh" --include-path ggml/include --generate "$BUILD_OLD" "${OLD_LIBS[@]}") +(cd "$REPO_ROOT" && "$SCRIPT_DIR/check-apiabi-compat.sh" --include-path ggml/include --generate "$BUILD_NEW" "${NEW_LIBS[@]}") +(cd "$REPO_ROOT" && "$SCRIPT_DIR/check-apiabi-compat.sh" "${CHECK_FLAGS[@]}" --check "$BUILD_OLD" "$BUILD_NEW") diff --git a/scripts/make-release-checks.sh b/scripts/make-release-checks.sh index d78fa1457c..2b60e870fa 100755 --- a/scripts/make-release-checks.sh +++ b/scripts/make-release-checks.sh @@ -165,6 +165,23 @@ else fi fi +echo "Checking API/ABI compatibility..." +set +e +bash "$SCRIPT_DIR/check-release-apiabi.sh" +APIABI_RESULT=$? +set -e +if [[ $APIABI_RESULT -ne 0 ]]; then + if [[ "$DRY_RUN" == "true" ]]; then + echo "Warning: API/ABI check found backwards-incompatible changes (dry run, continuing)." + CHECKS_PASSED=false + else + echo "Error: API/ABI check found backwards-incompatible changes." + exit 1 + fi +else + echo "API/ABI compatibility check passed - OK" +fi + if [[ -n "${GITHUB_OUTPUT:-}" ]]; then echo "checks_passed=${CHECKS_PASSED}" >> "$GITHUB_OUTPUT" fi