diff --git a/.abicheck.yml b/.abicheck.yml new file mode 100644 index 000000000..b790f090c --- /dev/null +++ b/.abicheck.yml @@ -0,0 +1,35 @@ +# Copyright 2026 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. + +# abicheck configuration, picked up from the repo root by `abicheck compare`. +# Validated against the abicheck 0.5.0 config schema. +# +# Kept identical to .abicheck.yml in the innersource repo: the two repos build +# the same runtime library and must not disagree on how it is parsed. + +# castxml's default cc-gnu detection parses SVS's C++20 headers at the wrong +# standard (std::span / constexpr dtor fail). The clang AST frontend handles +# C++20 cleanly; layout-only checks fall back to the .so's DWARF. +compile: + frontend: clang + std: c++20 + +# Scope the compared surface to what we publish, and collapse the std/gnu_cxx +# template-instantiation noise that leaks past --exclude-libs,ALL. +scope: + public: true + collapse_versioned_symbols: true + +suppression: + require_justification: true diff --git a/.github/abi-baseline-tag b/.github/abi-baseline-tag new file mode 100644 index 000000000..8eb83dd30 --- /dev/null +++ b/.github/abi-baseline-tag @@ -0,0 +1,27 @@ +# Copyright 2026 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. + +# The released tag that runtime-bindings ABI compatibility is measured against. +# Assets are pulled from this repo's GitHub releases at check time. +# +# Bumped deliberately as part of cutting a release, so that resetting the ABI +# baseline is a reviewable act rather than a side effect of tagging. Pinning also +# keeps a new release from silently moving the target under already-open PRs. +# +# `latest` is accepted and resolves dynamically, but is not the default: patch +# tags can land out of order (v0.3.1 and v0.3.2 were both tagged after v0.4.0), +# so "newest tag" is not always "current release line". +# +# Keep in sync with .github/abi-baseline-tag in the innersource repo. +v0.4.0 diff --git a/.github/abi-suppressions.yml b/.github/abi-suppressions.yml new file mode 100644 index 000000000..cf2819f78 --- /dev/null +++ b/.github/abi-suppressions.yml @@ -0,0 +1,82 @@ +# Copyright 2026 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. + +# ABI check suppression rules for the SVS C++ runtime bindings, used by +# napetrov/abicheck. Owned here rather than in the innersource repo so that both +# repos measure libsvs_runtime.so against the same rules -- a suppression present +# on one side only would make the innersource-vs-public equivalence check report +# a difference that is really just an asymmetric filter. +# +# Scope is the runtime bindings alone. The innersource repo keeps its own file +# for libsvs_shared_library.so, which leaks a far larger third-party surface. +# +# Measured on libsvs_runtime.so v0.4.0: 253 exported symbols, of which 55 are the +# declared svs::runtime::v0 API. Nearly everything suppressed below is a +# libstdc++ template instantiation that reached the export table because the +# bindings are built without -fvisibility=hidden. + +version: 1 +suppressions: + # --- C++ standard library / runtime (never part of the SVS API) --- + - symbol_pattern: "^_ZNSt.*" + reason: "C++ standard library symbols (leaked template instantiations)" + label: "stdlib" + + - symbol_pattern: "^_ZSt.*" + reason: "C++ standard library functions" + label: "stdlib" + + - symbol_pattern: "^_ZN9__gnu_cxx.*" + reason: "libstdc++ __gnu_cxx internals (numeric_traits etc.), leaked" + label: "stdlib" + + # RTTI/vtable/VTT for std types mangle as _ZTI/_ZTS/_ZTV/_ZTT, not _ZNSt. + - symbol_pattern: "^_ZT[VIST]N?St.*" + reason: "RTTI/vtable/VTT for std:: types (e.g. thread::_State_impl, future)" + label: "stdlib" + + - symbol_pattern: "^_ZT[VIST].*__gnu_cxx.*" + reason: "RTTI/vtable for __gnu_cxx types, leaked" + label: "stdlib" + + - symbol_pattern: "^__cxa_.*" + reason: "C++ ABI runtime symbols" + label: "cxx-abi" + + - symbol_pattern: "^__gxx_.*" + reason: "GCC C++ runtime symbols" + label: "cxx-abi" + + # --- Header-only third-party deps that leak via template instantiation --- + - symbol_pattern: "^_ZN[0-9]+toml.*" + reason: "toml++ header-only symbols leaked via instantiation" + label: "third-party" + + - symbol_pattern: "^_ZN3fmt.*" + reason: "fmt header-only symbols leaked via instantiation" + label: "third-party" + + - symbol_pattern: "^_ZN3tsl.*" + reason: "tsl::robin_map header-only symbols leaked via instantiation" + label: "third-party" + + - symbol_pattern: "^_ZN6spdlog.*" + reason: "spdlog header-only symbols leaked via instantiation" + label: "third-party" + + # --- svs::runtime::v0 and svs::...detail...: deliberately NOT suppressed --- + # The 55 declared v0 functions are the whole point of the check. `detail` + # symbols are not suppressed either: users reach them transitively through + # template instantiation, so a vtable or layout change there is a real break. + # See the same note in the innersource suppression file. diff --git a/.github/scripts/abi-check.sh b/.github/scripts/abi-check.sh new file mode 100755 index 000000000..6cfd23073 --- /dev/null +++ b/.github/scripts/abi-check.sh @@ -0,0 +1,126 @@ +#!/bin/bash +# Copyright 2026 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. + +# Compare the ABI of two SVS build tarballs with napetrov/abicheck. +# +# One primitive serves both questions asked of it: +# temporal old = a release tarball, new = this build -- did we break users? +# equivalence old and new are the same source revision built two ways -- did +# the build paths diverge? +# The distinction is in what the caller passes, not in the logic, so this lives +# in one place. It is called from this repo's build-cpp-runtime-bindings.yml and +# from build-share-lib.yml in the innersource repo via the submodule path. +# +# Usage: abi-check.sh +# +# Overridable via environment: +# LIBRARY library basename to compare (default libsvs_runtime.so) +# HEADER_SUBDIR header root inside the tarball (default include/svs/runtime) +# SUPPRESSIONS suppression file (default .github/abi-suppressions.yml) +# POLICY abicheck policy (default strict_abi) +# REPORT markdown report path (default abi-report.md) +# WORKDIR scratch directory (default abi-work) +# +# Exit status is abicheck's: 0 compatible, 2 source-level API break, 4 binary +# ABI break, 64 invalid invocation. 77 is added to mean "an input was missing", +# which is not a finding and must never be reported as one. + +set -euo pipefail + +OLD_LABEL="${1:?Usage: abi-check.sh }" +OLD_TARBALL="${2:?missing old tarball}" +NEW_LABEL="${3:?missing new label}" +NEW_TARBALL="${4:?missing new tarball}" + +LIBRARY="${LIBRARY:-libsvs_runtime.so}" +HEADER_SUBDIR="${HEADER_SUBDIR:-include/svs/runtime}" +SUPPRESSIONS="${SUPPRESSIONS:-.github/abi-suppressions.yml}" +POLICY="${POLICY:-strict_abi}" +REPORT="${REPORT:-abi-report.md}" +WORKDIR="${WORKDIR:-abi-work}" + +# Prints the resolved library path on success. Release tarballs and CI artifacts +# have differed on whether the unversioned symlink is included, so accept the +# versioned real file too. +unpack() { + local tarball="$1" dest="$2" label="$3" lib + + if [ ! -f "$tarball" ]; then + echo "::warning::$label: tarball not found at $tarball" >&2 + return 1 + fi + + rm -rf "$dest" + mkdir -p "$dest" + tar -xzf "$tarball" -C "$dest" + + lib=$(find "$dest" -name "$LIBRARY" -o -name "$LIBRARY.*" | sort | head -1) + if [ -z "$lib" ]; then + echo "::error::$label: $LIBRARY not found in $tarball" >&2 + return 1 + fi + if [ ! -d "$dest/$HEADER_SUBDIR" ]; then + echo "::error::$label: headers missing from $tarball ($HEADER_SUBDIR)" >&2 + return 1 + fi + echo "$lib" +} + +OLD_DIR="$WORKDIR/old" +NEW_DIR="$WORKDIR/new" + +OLD_LIB=$(unpack "$OLD_TARBALL" "$OLD_DIR" "$OLD_LABEL") || exit 77 +NEW_LIB=$(unpack "$NEW_TARBALL" "$NEW_DIR" "$NEW_LABEL") || exit 77 + +echo "Comparing $LIBRARY: [$OLD_LABEL] -> [$NEW_LABEL]" + +suppress_args=() +[ -f "$SUPPRESSIONS" ] && suppress_args=(--suppress "$SUPPRESSIONS") + +rc=0 +# Without pipefail the pipe into tee masks abicheck's exit status. +set -o pipefail +# castxml is not installed on the runners; clang is. `auto` does not fall back to +# clang on its own. If header parsing degrades, abicheck still reports from the +# ELF tier -- which is where vtable-size findings come from anyway. +abicheck compare \ + "$OLD_LIB" \ + "$NEW_LIB" \ + --header old="$OLD_DIR/$HEADER_SUBDIR" \ + --header new="$NEW_DIR/$HEADER_SUBDIR" \ + --include old="$OLD_DIR/include" \ + --include new="$NEW_DIR/include" \ + --version old="$OLD_LABEL" \ + --version new="$NEW_LABEL" \ + --ast-frontend clang \ + --gcc-options "-std=c++20 -include cstddef" \ + --policy "$POLICY" \ + "${suppress_args[@]}" \ + --format markdown | tee "$REPORT" || rc=$? + +case "$rc" in + 0) + echo "ABI compatible: $OLD_LABEL -> $NEW_LABEL" + ;; + 64) + echo "::error::abicheck rejected the invocation (exit 64) comparing" \ + "$OLD_LABEL -> $NEW_LABEL. This is a harness bug, not an ABI finding." + ;; + *) + echo "::warning::ABI incompatibility: $NEW_LABEL differs from $OLD_LABEL (rc=$rc)" + ;; +esac + +exit "$rc" diff --git a/.github/workflows/build-cpp-runtime-bindings.yml b/.github/workflows/build-cpp-runtime-bindings.yml index b87cd0ab5..330abd19f 100644 --- a/.github/workflows/build-cpp-runtime-bindings.yml +++ b/.github/workflows/build-cpp-runtime-bindings.yml @@ -133,3 +133,102 @@ jobs: -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 last published release. + # + # Only the default variant is checked: releases publish + # svs-cpp-runtime-bindings.tar.gz but no -public-only asset, so that variant has + # nothing to compare against. It is covered indirectly -- 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 the last release + needs: build-cpp-runtime-bindings + runs-on: ubuntu-22.04 + # 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.5.*' + + - name: Resolve baseline release tag + id: baseline + env: + GH_TOKEN: ${{ github.token }} + run: | + tag=$(grep -v '^#' .github/abi-baseline-tag | tr -d '[:space:]') + if [ "$tag" = 'latest' ]; then + tag=$(gh release view --json tagName --jq '.tagName') + fi + echo "tag=$tag" >> "$GITHUB_OUTPUT" + + # A missing baseline is a hard failure: a green check that silently skipped + # the comparison is worse than a red one. + - name: Download release baseline + env: + GH_TOKEN: ${{ github.token }} + TAG: ${{ steps.baseline.outputs.tag }} + run: | + mkdir -p baseline + if ! gh release download "$TAG" --pattern svs-cpp-runtime-bindings.tar.gz \ + --dir baseline; then + echo "::error::Release $TAG has no svs-cpp-runtime-bindings.tar.gz asset." \ + "Fix the pin in .github/abi-baseline-tag." + exit 1 + fi + + - 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.tag }}' 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.tag }}" + 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"