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