Skip to content

Add ABI checking

Add ABI checking #914

# Copyright 2025 Intel Corporation
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
name: Build and test C++ Runtime Bindings
on:
push:
branches:
- main
pull_request:
workflow_dispatch:
permissions:
contents: read
# This allows a subsequently queued workflow run to interrupt previous runs
concurrency:
group: '${{ github.workflow }} @ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}'
cancel-in-progress: true
jobs:
build-cpp-runtime-bindings:
name: Build and unit tests for C++ runtime bindings
runs-on: ubuntu-22.04
strategy:
matrix:
include:
- name: "with static library"
enable_lvq_leanvec: "ON"
require_lto: "ON"
suffix: ""
- name: "public only"
enable_lvq_leanvec: "OFF"
require_lto: "OFF"
suffix: "-public-only"
fail-fast: false
steps:
- uses: actions/checkout@v6
- name: Build Docker image
run: |
docker build -t svs-manylinux228:latest -f docker/x86_64/manylinux228/Dockerfile .
- name: Build libraries in Docker container
run: |
docker run --rm \
-v ${{ github.workspace }}:/workspace \
-w /workspace \
-e ENABLE_LVQ_LEANVEC=${{ matrix.enable_lvq_leanvec }} \
-e REQUIRE_LTO_ARCHIVE=${{ matrix.require_lto }} \
-e SUFFIX=${{ matrix.suffix }} \
svs-manylinux228:latest \
/bin/bash .github/scripts/build-cpp-runtime-bindings.sh
- name: Upload cpp runtime bindings artifacts
uses: actions/upload-artifact@v7
with:
name: svs-cpp-runtime-bindings${{ matrix.suffix }}
path: svs-cpp-runtime-bindings${{ matrix.suffix }}.tar.gz
# Reduced due to size, but kept longer on main: those artifacts are the
# ABI baseline every open PR is measured against, so expiring them in a
# week makes the check fail on any quiet stretch.
retention-days: ${{ github.ref == 'refs/heads/main' && 30 || 7 }}
- name: Upload conda package artifacts
uses: actions/upload-artifact@v7
with:
name: libsvs-runtime-conda${{ matrix.suffix }}
path: conda-bld/linux-64/libsvs-runtime-*.conda
retention-days: 7
# Run unit tests that were built as part of this job
- name: Run unit tests in Docker container
run: |
docker run --rm \
-v ${{ github.workspace }}:/workspace \
-w /workspace \
-e ENABLE_LVQ_LEANVEC=${{ matrix.enable_lvq_leanvec }} \
svs-manylinux228:latest \
/bin/bash /workspace/.github/scripts/test-cpp-runtime-unit.sh
# Run full test script using the built artifacts
test:
name: Integration tests for C++ runtime
needs: build-cpp-runtime-bindings
runs-on: ubuntu-22.04
strategy:
matrix:
# enable_lvq_leanvec must match the build job's value for this suffix; if they
# disagree, test-faiss.sh expects the wrong outcome from the downloaded artifact.
include:
- name: "with static library"
enable_lvq_leanvec: "ON"
suffix: ""
- name: "public only"
enable_lvq_leanvec: "OFF"
suffix: "-public-only"
fail-fast: false
steps:
- uses: actions/checkout@v6
- name: Build Docker image
run: |
docker build -t svs-manylinux228:latest -f docker/x86_64/manylinux228/Dockerfile .
# Need to download for a new job
- name: Download conda package
uses: actions/download-artifact@v8
with:
name: libsvs-runtime-conda${{ matrix.suffix }}
path: runtime_conda
- name: List available artifacts
run: |
ls -la runtime_conda/
- name: Test in Docker container
run: |
docker run --rm \
-v ${{ github.workspace }}:/workspace \
-v ${{ github.workspace }}/runtime_conda:/runtime_conda \
-w /workspace \
-e ENABLE_LVQ_LEANVEC=${{ matrix.enable_lvq_leanvec }} \
svs-manylinux228:latest \
/bin/bash .github/scripts/test-faiss.sh
# Check the built runtime bindings against the newest build on main.
#
# Deliberately not against a published release: release assets are produced by
# the innersource private-source build, which compiles svs in-tree with
# IVF/LeanVec, while this repo's CI links the prebuilt LTO archive. Those two
# paths export genuinely different symbol sets (253 vs 138 on v0.4.0), so a
# release baseline reports build-path divergence as ABI findings and needs
# standing waivers to stay green. main-vs-PR is like-for-like by construction.
#
# The trade-off is real and accepted: this measures drift since main, not
# compatibility with what users have installed. Nothing checks the latter per-PR
# any more -- it is a release-time comparison against the previous release's
# assets, and has to be run deliberately when cutting one.
#
# Only the default variant is checked; -public-only is covered indirectly since
# both variants build from the same headers.
#
# The comparison primitive is .github/scripts/abi-check.sh, which the
# innersource repo also calls through its submodule path so the two repos cannot
# drift on how the same library is measured.
abi-check:
name: ABI check against main
needs: build-cpp-runtime-bindings
runs-on: ubuntu-22.04
permissions:
contents: read
actions: read # gh run list/download, to fetch main's artifact as baseline
# An intentionally-breaking PR still gets a full report: the label downgrades
# this job from blocking to informational rather than skipping it, so the
# break is recorded on the PR instead of going unmeasured.
continue-on-error: ${{ contains(toJson(github.event.pull_request.labels.*.name), '"API/ABI breaking change"') }}
steps:
- uses: actions/checkout@v6
# The hosted ubuntu-22.04 image ships only clang-13/14, which degrades
# abicheck's clang AST frontend to ELF-tier and drops constrained-template
# diffs. innersource gets a c++20-capable clang from its self-hosted
# toolchain; this keeps the two repos measuring at the same fidelity.
- name: Install clang for the AST frontend
run: |
wget -qO /tmp/llvm.sh https://apt.llvm.org/llvm.sh
chmod +x /tmp/llvm.sh
sudo /tmp/llvm.sh 18
sudo apt-get install -y --no-install-recommends libclang-18-dev llvm-18-dev
sudo update-alternatives --install /usr/bin/clang clang /usr/bin/clang-18 100
sudo update-alternatives --install /usr/bin/clang++ clang++ /usr/bin/clang++-18 100
- name: Install abicheck
run: pip install 'abicheck==0.6.*'
# The newest main run that still has a downloadable artifact. This is main's
# tip rather than the PR's true merge-base: a PR far behind main will also
# see main's own changes, which is noisy but never a false "compatible".
#
# A missing baseline is a hard failure: a green check that silently skipped
# the comparison is worse than a red one.
- name: Download baseline build from main
id: baseline
env:
GH_TOKEN: ${{ github.token }}
run: |
mkdir -p baseline
# Not filtered on conclusion: a run whose tests failed still produced a
# valid tarball, and a run that produced none just fails to download and
# falls through to the next candidate.
#
# The REST endpoint rather than `gh run list`: the latter was observed
# returning stale pages (all candidates months-expired, newest run
# missing), which burns every attempt and fails the leg spuriously.
candidates=$(gh api \
"repos/$GITHUB_REPOSITORY/actions/workflows/build-cpp-runtime-bindings.yml/runs?branch=main&event=push&per_page=20" \
--jq '.workflow_runs[] | "\(.id) \(.head_sha)"')
label=
while read -r run_id sha; do
[ -n "$run_id" ] || continue
[ "$run_id" != "$GITHUB_RUN_ID" ] || continue
if gh run download "$run_id" --repo "$GITHUB_REPOSITORY" \
--name svs-cpp-runtime-bindings --dir baseline; then
label="main@${sha:0:12}"
echo "Baseline: run $run_id ($label)"
break
fi
done <<< "$candidates"
if [ -z "$label" ]; then
echo "::error::No recent main run has a downloadable" \
"svs-cpp-runtime-bindings artifact. Re-run this workflow on main" \
"to refresh the baseline."
exit 1
fi
echo "label=$label" >> "$GITHUB_OUTPUT"
- name: Download this build
uses: actions/download-artifact@v8
with:
name: svs-cpp-runtime-bindings
path: current
- name: Compare
env:
LIBRARY: libsvs_runtime.so
HEADER_SUBDIR: include/svs/runtime
SUPPRESSIONS: .github/abi-suppressions.yml
POLICY: strict_abi
run: |
chmod +x .github/scripts/abi-check.sh
label='${{ github.event.pull_request.number && format('PR {0}', github.event.pull_request.number) || github.ref_name }}'
rc=0
.github/scripts/abi-check.sh \
'${{ steps.baseline.outputs.label }}' baseline/svs-cpp-runtime-bindings.tar.gz \
"$label" current/svs-cpp-runtime-bindings.tar.gz || rc=$?
if [ -s abi-report.md ]; then
{
echo "## ABI vs ${{ steps.baseline.outputs.label }}"
cat abi-report.md
} >> "$GITHUB_STEP_SUMMARY"
fi
# 77 means an input tarball was missing; that is a harness/plumbing
# problem and must fail loudly rather than pass as "no findings".
if [ "$rc" -eq 77 ] || [ "$rc" -eq 64 ]; then
echo "::error::ABI check could not run (rc=$rc)."
exit 1
fi
exit "$rc"