diff --git a/.github/scripts/build-c-api-bindings.sh b/.github/scripts/build-c-api-bindings.sh new file mode 100755 index 00000000..e0a8a495 --- /dev/null +++ b/.github/scripts/build-c-api-bindings.sh @@ -0,0 +1,58 @@ +#!/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. + +# Configure, build, install and package the C API bindings. +# +# Inputs (all optional, with defaults suitable for a local run): +# ENABLE_LVQ_LEANVEC ON to statically link the LVQ/LeanVec backend +# REQUIRE_LTO_ARCHIVE ON to fail (not warn) if the compiler can't consume the +# LTO archive; set in CI, left off for local builds +# SUFFIX artifact name suffix (e.g. -public-only) +# WORKSPACE repository root; defaults to this script's repo so it +# also runs outside the container + +set -e + +# In the manylinux/rockylinux containers the pinned gcc-toolset lives behind an +# scl profile script; harmless no-op on a plain runner. +source /etc/bashrc 2>/dev/null || true + +# Repo root, derived from this script's location so no git metadata is needed. +WORKSPACE="${WORKSPACE:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}" +BUILD_DIR="${WORKSPACE}/build_c_api" +INSTALL_DIR="${WORKSPACE}/install_c_api" +ENABLE_LVQ_LEANVEC="${ENABLE_LVQ_LEANVEC:-OFF}" +REQUIRE_LTO_ARCHIVE="${REQUIRE_LTO_ARCHIVE:-OFF}" + +echo "compiler: $(${CXX:-c++} --version | head -1)" + +rm -rf "${BUILD_DIR}" "${INSTALL_DIR}" + +cmake -B"${BUILD_DIR}" -S"${WORKSPACE}/bindings/c" \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_INSTALL_PREFIX="${INSTALL_DIR}" \ + -DCMAKE_INSTALL_LIBDIR=lib \ + -DSVS_BUILD_C_API_TESTS=ON \ + -DSVS_RUNTIME_ENABLE_LVQ_LEANVEC="${ENABLE_LVQ_LEANVEC}" \ + -DSVS_REQUIRE_LTO_ARCHIVE="${REQUIRE_LTO_ARCHIVE}" + +cmake --build "${BUILD_DIR}" -j"$(nproc)" + +# Install only the C API component: the dependency headers that a full install +# would also emit are not part of the shipped interface. +cmake --install "${BUILD_DIR}" --component C_API + +tar -czf "${WORKSPACE}/svs-c-api${SUFFIX}.tar.gz" -C "${INSTALL_DIR}" . +echo "Packaged ${WORKSPACE}/svs-c-api${SUFFIX}.tar.gz" diff --git a/.github/scripts/build-cpp-runtime-bindings.sh b/.github/scripts/build-cpp-runtime-bindings.sh index c454eb72..bbe56509 100644 --- a/.github/scripts/build-cpp-runtime-bindings.sh +++ b/.github/scripts/build-cpp-runtime-bindings.sh @@ -50,6 +50,7 @@ CMAKE_ARGS=( "-DCMAKE_INSTALL_PREFIX=/workspace/install_cpp_bindings" "-DCMAKE_INSTALL_LIBDIR=lib" "-DSVS_RUNTIME_ENABLE_LVQ_LEANVEC=${ENABLE_LVQ_LEANVEC:-ON}" + "-DSVS_REQUIRE_LTO_ARCHIVE=${REQUIRE_LTO_ARCHIVE:-OFF}" "-DSVS_RUNTIME_ENABLE_IVF=ON" "-DSVS_EXPERIMENTAL_CLANG_TIDY=ON" ) diff --git a/.github/scripts/test-c-api-bindings.sh b/.github/scripts/test-c-api-bindings.sh new file mode 100755 index 00000000..81476a1e --- /dev/null +++ b/.github/scripts/test-c-api-bindings.sh @@ -0,0 +1,85 @@ +#!/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. + +# Integration test for the packaged C API: verifies the tarball is a usable +# package rather than just a successful compile. Runs against the artifact only, +# with no access to the build tree. +# +# Inputs: +# SUFFIX artifact name suffix (e.g. -public-only) +# WORKSPACE repository root; defaults to this script's repo so it also runs +# outside the container + +set -e + +# Match build-c-api-bindings.sh: pick up the container's pinned gcc-toolset. +source /etc/bashrc 2>/dev/null || true + +# Repo root, derived from this script's location so no git metadata is needed. +WORKSPACE="${WORKSPACE:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}" +STAGE_DIR="${WORKSPACE}/c_api_integration" + +# Prefer the artifact downloaded by the workflow, else a tarball built locally. +TARBALL="${WORKSPACE}/c_api_artifact/svs-c-api${SUFFIX}.tar.gz" +if [ ! -e "${TARBALL}" ]; then + TARBALL="${WORKSPACE}/svs-c-api${SUFFIX}.tar.gz" +fi + +INSTALL_DIR="${STAGE_DIR}/install" +CONSUMER_BUILD="${STAGE_DIR}/consumer-build" + +rm -rf "${STAGE_DIR}" +mkdir -p "${INSTALL_DIR}" +tar -xzf "${TARBALL}" -C "${INSTALL_DIR}" + +echo "::group::Package contents" +find "${INSTALL_DIR}" -type f -o -type l | sort +echo "::endgroup::" + +LIBDIR="${INSTALL_DIR}/lib" +LIB="${LIBDIR}/libsvs_c_api.so" +if [ ! -e "${LIB}" ]; then + echo "ERROR: ${LIB} missing from the package" + exit 1 +fi + +echo "::group::Strong exported symbols" +nm -D --defined-only "${LIB}" | awk '$2=="T"{print $3}' | sort +echo "::endgroup::" + +# Only the documented svs_* C ABI may be exported with strong linkage. This also +# guards the statically linked LVQ/LeanVec backend against leaking symbols. +# +# std:: template instantiations (_ZNSt/_ZSt) are excluded: GCC emits some of these +# with strong linkage from the LTO archive, and they are standard-library code +# rather than SVS implementation detail. The check still catches any leak of an +# actual svs/proprietary internal. +LEAKED=$(nm -D --defined-only "${LIB}" | awk '$2=="T"{print $3}' \ + | grep -v '^svs_' | grep -vE '^_Z+(N?)St' || true) +if [ -n "${LEAKED}" ]; then + echo "ERROR: non-svs_ symbols exported from the C API:" + echo "${LEAKED}" + exit 1 +fi + +# Build a standalone C project against the installed CMake package, the way a +# downstream integration would. Catches exported-target defects (a missing +# find_dependency, or a C++ requirement leaking onto a C consumer) that a +# build-tree-only test cannot see. +cmake -B"${CONSUMER_BUILD}" -S"${WORKSPACE}/bindings/c/tests/consumer" \ + -DCMAKE_PREFIX_PATH="${INSTALL_DIR}" +cmake --build "${CONSUMER_BUILD}" + +LD_LIBRARY_PATH="${LIBDIR}" "${CONSUMER_BUILD}/c_api_consumer" diff --git a/.github/scripts/test-c-api-unit.sh b/.github/scripts/test-c-api-unit.sh new file mode 100755 index 00000000..7ea6e322 --- /dev/null +++ b/.github/scripts/test-c-api-unit.sh @@ -0,0 +1,47 @@ +#!/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. + +# Run the C API unit tests and samples out of an existing build tree. +# +# Inputs: +# WORKSPACE repository root; defaults to this script's repo so it also runs +# outside the container + +set -e + +# Match build-c-api-bindings.sh: pick up the container's pinned gcc-toolset. +source /etc/bashrc 2>/dev/null || true + +# Repo root, derived from this script's location so no git metadata is needed. +WORKSPACE="${WORKSPACE:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}" +BUILD_DIR="${WORKSPACE}/build_c_api" + +# LVQ/LeanVec need a specific ISA. The tests already accept +# SVS_ERROR_UNSUPPORTED_HW (but never SVS_ERROR_NOT_IMPLEMENTED), so this is +# reported for triage rather than used to skip anything. +echo "vendor: $(grep -m1 vendor_id /proc/cpuinfo || echo unknown)" +echo "model: $(grep -m1 'model name' /proc/cpuinfo || echo unknown)" +echo "avx512: $(grep -o 'avx512[a-z_0-9]*' /proc/cpuinfo | sort -u | tr '\n' ' ')" + +ctest --test-dir "${BUILD_DIR}" --output-on-failure --no-tests=error + +# The samples are the only executable check that the public headers are usable +# from C and that an end-to-end build/search runs. They regressed to a non-zero +# exit once already, so they are part of the gate. +for sample in c_api_simple c_api_save_load c_api_dynamic; do + echo "::group::${sample}" + "${BUILD_DIR}/samples/${sample}" + echo "::endgroup::" +done diff --git a/.github/workflows/build-c-api-bindings.yml b/.github/workflows/build-c-api-bindings.yml index 782c06e1..a70c68ef 100644 --- a/.github/workflows/build-c-api-bindings.yml +++ b/.github/workflows/build-c-api-bindings.yml @@ -17,7 +17,7 @@ name: Build and test C API bindings on: push: branches: - - main + - main pull_request: workflow_dispatch: @@ -26,57 +26,98 @@ permissions: # 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 }} + group: '${{ github.workflow }} @ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}' cancel-in-progress: true jobs: - build: - name: ${{ matrix.cxx }}, ${{ matrix.build_type }} + build-c-api-bindings: + name: Build and unit tests for C API (${{ matrix.name }}) runs-on: ubuntu-22.04 strategy: matrix: - build_type: [RelWithDebInfo] - cxx: [g++-11, g++-12, clang++-15] + # Mirrors build-cpp-runtime-bindings.yml. include: - - cxx: g++-11 - cc: gcc-11 - - cxx: g++-12 - cc: gcc-12 - - cxx: clang++-15 - cc: clang-15 + - 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: Install OpenMP runtime - env: - CXX: ${{ matrix.cxx }} - run: | - sudo apt-get update - # The default libgomp shipped with GCC does not match the clang - # toolchain, so install the LLVM OpenMP runtime (libomp) for clang. - if [[ "${CXX}" == clang* ]]; then - sudo apt-get install -y libomp-15-dev - fi - - - name: Configure build - working-directory: ${{ runner.temp }} - env: - CXX: ${{ matrix.cxx }} - CC: ${{ matrix.cc }} - TEMP_WORKSPACE: ${{ runner.temp }} - run: | - cmake -B${TEMP_WORKSPACE}/build -S${GITHUB_WORKSPACE}/bindings/c \ - -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} \ - -DSVS_BUILD_C_API_TESTS=ON - - - name: Build C API, tests and samples - working-directory: ${{ runner.temp }}/build - run: make -j$(nproc) - - - name: Run C API tests - env: - CTEST_OUTPUT_ON_FAILURE: 1 - working-directory: ${{ runner.temp }}/build - run: ctest -C ${{ matrix.build_type }} --output-on-failure + - uses: actions/checkout@v6 + + - name: Build Docker image + run: docker build -t svs-manylinux228:latest -f docker/x86_64/manylinux228/Dockerfile . + + - name: Build C API bindings 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-c-api-bindings.sh + + - name: Upload C API bindings artifacts + uses: actions/upload-artifact@v7 + with: + name: svs-c-api${{ matrix.suffix }} + path: svs-c-api${{ matrix.suffix }}.tar.gz + 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 \ + svs-manylinux228:latest \ + /bin/bash /workspace/.github/scripts/test-c-api-unit.sh + + # Run integration tests against the packaged artifact. Eventually this should + # run the setup and test scope of the actual downstream integrations; for now it + # just confirms the tarball is functional - it installs, exports only the svs_* + # C ABI, and can be consumed from a standalone C project. + test: + name: Integration tests for C API (${{ matrix.name }}) + needs: build-c-api-bindings + runs-on: ubuntu-22.04 + strategy: + matrix: + include: + - name: "with static library" + suffix: "" + - name: "public only" + 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 C API package + uses: actions/download-artifact@v8 + with: + name: svs-c-api${{ matrix.suffix }} + path: c_api_artifact + + - name: List available artifacts + run: ls -la c_api_artifact/ + + - name: Test packaged C API in Docker container + run: | + docker run --rm \ + -v ${{ github.workspace }}:/workspace \ + -w /workspace \ + -e SUFFIX=${{ matrix.suffix }} \ + svs-manylinux228:latest \ + /bin/bash .github/scripts/test-c-api-bindings.sh diff --git a/.github/workflows/build-cpp-runtime-bindings.yml b/.github/workflows/build-cpp-runtime-bindings.yml index 468789bf..c715d24e 100644 --- a/.github/workflows/build-cpp-runtime-bindings.yml +++ b/.github/workflows/build-cpp-runtime-bindings.yml @@ -38,9 +38,11 @@ jobs: 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 @@ -57,6 +59,7 @@ jobs: -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 diff --git a/bindings/c/CMakeLists.txt b/bindings/c/CMakeLists.txt index b88ab589..c45de3fc 100644 --- a/bindings/c/CMakeLists.txt +++ b/bindings/c/CMakeLists.txt @@ -47,7 +47,10 @@ target_include_directories(${TARGET_NAME} PRIVATE ) find_package(OpenMP REQUIRED) -target_link_libraries(${TARGET_NAME} PUBLIC OpenMP::OpenMP_CXX) +# PRIVATE: OpenMP is an implementation detail linked into the shared library. +# Exporting it would force consumers of the C ABI to resolve a C++ OpenMP +# target they never asked for. +target_link_libraries(${TARGET_NAME} PRIVATE OpenMP::OpenMP_CXX) target_compile_options(${TARGET_NAME} PRIVATE -DSVS_ENABLE_OMP=1 @@ -59,7 +62,10 @@ if(UNIX AND NOT APPLE) target_link_options(${TARGET_NAME} PRIVATE "SHELL:-Wl,--exclude-libs,ALL") endif() -target_compile_features(${TARGET_NAME} INTERFACE cxx_std_20) +# C++20 is required to build this library, but not to consume it: the public +# surface is a C ABI. Keep the requirement PRIVATE so that pure-C consumers are +# not forced to compile as C++20. +target_compile_features(${TARGET_NAME} PRIVATE cxx_std_20) if (NOT DEFINED SVS_CXX_STANDARD OR SVS_CXX_STANDARD STREQUAL "") set(SVS_CXX_STANDARD 20) endif() @@ -73,6 +79,11 @@ target_link_libraries(${TARGET_NAME} PRIVATE svs::svs ) +# The non-LTO fallback below is correct but slower, so nothing fails and CI stays +# green. Set this where the LTO archive is the point (CI) to make the drop an error. +option(SVS_REQUIRE_LTO_ARCHIVE + "Fail instead of warn when the compiler cannot consume the LVQ/LeanVec LTO archive" OFF) + if (SVS_RUNTIME_ENABLE_LVQ_LEANVEC) message(STATUS "Enabling LVQ/LeanVec support in C API") target_compile_definitions(${TARGET_NAME} PRIVATE SVS_RUNTIME_ENABLE_LVQ_LEANVEC) @@ -106,20 +117,34 @@ if (SVS_RUNTIME_ENABLE_LVQ_LEANVEC) else() # Links to LTO-enabled static library, requires GCC/G++ 11.2 if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL "11.2" AND CMAKE_CXX_COMPILER_VERSION VERSION_LESS "11.3") - set(SVS_URL "https://github.com/intel/ScalableVectorSearch/releases/download/nightly/svs-shared-library-lto-nightly-2026-02-05-1017.tar.gz" + set(SVS_URL "https://github.com/intel/ScalableVectorSearch/releases/download/v0.4.0/svs-shared-library-lto.tar.gz" CACHE STRING "URL to download SVS shared library") else() - message(WARNING + # The fallback is correct but slower, so nothing downstream fails and CI + # stays green. Set SVS_REQUIRE_LTO_ARCHIVE=ON where the LTO archive is + # the point (the containerised CI job) to make the drop an error. + if(SVS_REQUIRE_LTO_ARCHIVE) + set(SVS_LTO_MESSAGE_LEVEL FATAL_ERROR) + else() + set(SVS_LTO_MESSAGE_LEVEL WARNING) + endif() + message(${SVS_LTO_MESSAGE_LEVEL} "Pre-built LVQ/LeanVec SVS library requires GCC/G++ v.11.2 to apply LTO optimizations." "Current compiler: ${CMAKE_CXX_COMPILER_ID} ${CMAKE_CXX_COMPILER_VERSION}" ) - set(SVS_URL "https://github.com/intel/ScalableVectorSearch/releases/download/nightly/svs-shared-library-nightly-2026-02-05-1017.tar.gz" + set(SVS_URL "https://github.com/intel/ScalableVectorSearch/releases/download/v0.4.0/svs-shared-library.tar.gz" CACHE STRING "URL to download SVS shared library") endif() include(FetchContent) + # DOWNLOAD_EXTRACT_TIMESTAMP needs CMake 3.24+; 3.22 is still around locally. + set(SVS_FETCH_EXTRA_ARGS) + if(CMAKE_VERSION VERSION_GREATER_EQUAL "3.24") + list(APPEND SVS_FETCH_EXTRA_ARGS DOWNLOAD_EXTRACT_TIMESTAMP TRUE) + endif() FetchContent_Declare( svs URL ${SVS_URL} + ${SVS_FETCH_EXTRA_ARGS} ) FetchContent_MakeAvailable(svs) list(APPEND CMAKE_PREFIX_PATH "${svs_SOURCE_DIR}") diff --git a/bindings/c/samples/dynamic.c b/bindings/c/samples/dynamic.c index 3de193fe..de76c6aa 100644 --- a/bindings/c/samples/dynamic.c +++ b/bindings/c/samples/dynamic.c @@ -130,6 +130,23 @@ int main() { // storage = svs_storage_create_sq(SVS_DATA_TYPE_INT8, error); + // LeanVec/LVQ are only available in builds that include the compression + // backend. When they are unavailable the build reports NOT_IMPLEMENTED (or + // UNSUPPORTED_HW on hardware lacking the required ISA); fall back to simple + // storage so this sample stays runnable against a public build. + if (!storage) { + svs_error_code_t code = svs_error_get_code(error); + if (code == SVS_ERROR_NOT_IMPLEMENTED || code == SVS_ERROR_UNSUPPORTED_HW) { + fprintf( + stderr, + "LeanVec storage unavailable (%s); falling back to simple float32 " + "storage.\n", + svs_error_get_message(error) + ); + storage = svs_storage_create_simple(SVS_DATA_TYPE_FLOAT32, error); + } + } + if (!storage) { fprintf(stderr, "Failed to create storage: %s\n", svs_error_get_message(error)); ret = 1; diff --git a/bindings/c/samples/save_load.c b/bindings/c/samples/save_load.c index 43aaf1ab..088267d1 100644 --- a/bindings/c/samples/save_load.c +++ b/bindings/c/samples/save_load.c @@ -130,6 +130,23 @@ int main() { // storage = svs_storage_create_sq(SVS_DATA_TYPE_INT8, error); + // LeanVec/LVQ are only available in builds that include the compression + // backend. When they are unavailable the build reports NOT_IMPLEMENTED (or + // UNSUPPORTED_HW on hardware lacking the required ISA); fall back to simple + // storage so this sample stays runnable against a public build. + if (!storage) { + svs_error_code_t code = svs_error_get_code(error); + if (code == SVS_ERROR_NOT_IMPLEMENTED || code == SVS_ERROR_UNSUPPORTED_HW) { + fprintf( + stderr, + "LeanVec storage unavailable (%s); falling back to simple float32 " + "storage.\n", + svs_error_get_message(error) + ); + storage = svs_storage_create_simple(SVS_DATA_TYPE_FLOAT32, error); + } + } + if (!storage) { fprintf(stderr, "Failed to create storage: %s\n", svs_error_get_message(error)); ret = 1; diff --git a/bindings/c/samples/simple.c b/bindings/c/samples/simple.c index da788465..38af55ba 100644 --- a/bindings/c/samples/simple.c +++ b/bindings/c/samples/simple.c @@ -120,6 +120,23 @@ int main() { // storage = svs_storage_create_sq(SVS_DATA_TYPE_INT8, error); + // LeanVec/LVQ are only available in builds that include the compression + // backend. When they are unavailable the build reports NOT_IMPLEMENTED (or + // UNSUPPORTED_HW on hardware lacking the required ISA); fall back to simple + // storage so this sample stays runnable against a public build. + if (!storage) { + svs_error_code_t code = svs_error_get_code(error); + if (code == SVS_ERROR_NOT_IMPLEMENTED || code == SVS_ERROR_UNSUPPORTED_HW) { + fprintf( + stderr, + "LeanVec storage unavailable (%s); falling back to simple float32 " + "storage.\n", + svs_error_get_message(error) + ); + storage = svs_storage_create_simple(SVS_DATA_TYPE_FLOAT32, error); + } + } + if (!storage) { fprintf(stderr, "Failed to create storage: %s\n", svs_error_get_message(error)); ret = 1; diff --git a/bindings/c/tests/CMakeLists.txt b/bindings/c/tests/CMakeLists.txt index 18645069..a8ca24db 100644 --- a/bindings/c/tests/CMakeLists.txt +++ b/bindings/c/tests/CMakeLists.txt @@ -61,6 +61,12 @@ target_link_libraries(${TARGET_NAME} PRIVATE Catch2::Catch2WithMain ) +# Tell the tests which storage backends this build is expected to provide, so +# LVQ/LeanVec assertions are real instead of accepting NOT_IMPLEMENTED. +if(SVS_RUNTIME_ENABLE_LVQ_LEANVEC) + target_compile_definitions(${TARGET_NAME} PRIVATE SVS_TEST_EXPECT_LVQ_LEANVEC) +endif() + # Set C++ standard target_compile_features(${TARGET_NAME} PRIVATE cxx_std_20) set_target_properties(${TARGET_NAME} PROPERTIES diff --git a/bindings/c/tests/c_api_index.cpp b/bindings/c/tests/c_api_index.cpp index 6bd19d29..3e6d6582 100644 --- a/bindings/c/tests/c_api_index.cpp +++ b/bindings/c/tests/c_api_index.cpp @@ -231,23 +231,22 @@ CATCH_TEST_CASE("C API Index Build and Search", "[c_api][index][build][search]") DIMENSION / 2, SVS_DATA_TYPE_INT4, SVS_DATA_TYPE_INT8, error ); CATCH_REQUIRE(check_storage_support(storage, error) == true); - if (storage != nullptr) { + if (storage_usable(storage)) { run_build_and_search(storage); } // LVQ: primary = int4, residual = int8 storage = svs_storage_create_lvq(SVS_DATA_TYPE_INT4, SVS_DATA_TYPE_INT8, error); CATCH_REQUIRE(check_storage_support(storage, error) == true); - if (storage != nullptr) { + if (storage_usable(storage)) { run_build_and_search(storage); } - // Scalar Quantization: int8 + // Scalar Quantization is available in every build - require it to work. storage = svs_storage_create_sq(SVS_DATA_TYPE_INT8, error); - CATCH_REQUIRE(check_storage_support(storage, error) == true); - if (storage != nullptr) { - run_build_and_search(storage); - } + CATCH_REQUIRE(storage != nullptr); + CATCH_REQUIRE(svs_error_ok(error)); + run_build_and_search(storage); svs_error_free(error); } diff --git a/bindings/c/tests/c_api_storage.cpp b/bindings/c/tests/c_api_storage.cpp index 22953e44..da7aae6c 100644 --- a/bindings/c/tests/c_api_storage.cpp +++ b/bindings/c/tests/c_api_storage.cpp @@ -132,8 +132,10 @@ CATCH_TEST_CASE("C API Storage", "[c_api][storage]") { CATCH_SECTION("Scalar Quantization Storage UINT8") { svs_error_h error = svs_error_create(); + // Scalar quantization is part of the public build - always expect success. svs_storage_h storage = svs_storage_create_sq(SVS_DATA_TYPE_UINT8, error); - CATCH_REQUIRE(check_storage_support(storage, error) == true); + CATCH_REQUIRE(storage != nullptr); + CATCH_REQUIRE(svs_error_ok(error)); svs_storage_free(storage); svs_error_free(error); @@ -142,8 +144,10 @@ CATCH_TEST_CASE("C API Storage", "[c_api][storage]") { CATCH_SECTION("Scalar Quantization Storage INT8") { svs_error_h error = svs_error_create(); + // Scalar quantization is part of the public build - always expect success. svs_storage_h storage = svs_storage_create_sq(SVS_DATA_TYPE_INT8, error); - CATCH_REQUIRE(check_storage_support(storage, error) == true); + CATCH_REQUIRE(storage != nullptr); + CATCH_REQUIRE(svs_error_ok(error)); svs_storage_free(storage); svs_error_free(error); diff --git a/bindings/c/tests/c_api_test_utils.h b/bindings/c/tests/c_api_test_utils.h index c1f488f4..3920d039 100644 --- a/bindings/c/tests/c_api_test_utils.h +++ b/bindings/c/tests/c_api_test_utils.h @@ -133,11 +133,32 @@ inline float cosine_distance(const float* a, const float* b, size_t dim) { return dot_product / (std::sqrt(norm_a) * std::sqrt(norm_b)); } +/// Check that a compressed-storage constructor behaved as this build should. +/// +/// Previously this accepted SVS_ERROR_NOT_IMPLEMENTED unconditionally, which made +/// every LVQ/LeanVec assertion pass vacuously in a public build - the tests could +/// not distinguish "compression works" from "compression is absent". The expected +/// outcome depends on how the library was configured: +/// +/// * compression compiled in -> success, or SVS_ERROR_UNSUPPORTED_HW when the +/// host CPU lacks the required ISA. NOT_IMPLEMENTED is a failure here. +/// * compression compiled out -> exactly SVS_ERROR_NOT_IMPLEMENTED. Silently +/// succeeding would mean the build flag did not take effect. inline bool check_storage_support(svs_storage_h storage, svs_error_h error) { - if (storage == nullptr) { - auto code = svs_error_get_code(error); - return code == SVS_ERROR_NOT_IMPLEMENTED || code == SVS_ERROR_UNSUPPORTED_HW; - } else { +#ifdef SVS_TEST_EXPECT_LVQ_LEANVEC + if (storage != nullptr) { return svs_error_ok(error) == true; } + // Accept only a genuine hardware limitation, never a missing implementation. + return svs_error_get_code(error) == SVS_ERROR_UNSUPPORTED_HW; +#else + if (storage != nullptr) { + return false; // compression should not be available in a public build + } + return svs_error_get_code(error) == SVS_ERROR_NOT_IMPLEMENTED; +#endif } + +/// True when compressed storage is expected to be usable on this host, so callers +/// can skip the build/search portion of a test that cannot run. +inline bool storage_usable(svs_storage_h storage) { return storage != nullptr; } diff --git a/bindings/c/tests/consumer/CMakeLists.txt b/bindings/c/tests/consumer/CMakeLists.txt new file mode 100644 index 00000000..c02eb977 --- /dev/null +++ b/bindings/c/tests/consumer/CMakeLists.txt @@ -0,0 +1,26 @@ +# 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. + +# Standalone project that consumes the *installed* C API package the same way a +# downstream integration would. It is deliberately not part of the C API build: +# it must be configured against an install tree so that a broken exported +# target (a missing find_dependency, or a C++ requirement leaking onto a C +# consumer) fails here instead of in the downstream project. +cmake_minimum_required(VERSION 3.21) +project(svs_c_api_consumer LANGUAGES C) + +find_package(svs_c_api REQUIRED) + +add_executable(c_api_consumer main.c) +target_link_libraries(c_api_consumer PRIVATE svs::svs_c_api) diff --git a/bindings/c/tests/consumer/main.c b/bindings/c/tests/consumer/main.c new file mode 100644 index 00000000..6f6cc0b3 --- /dev/null +++ b/bindings/c/tests/consumer/main.c @@ -0,0 +1,87 @@ +/* + * 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. + */ + +/* + * Smoke test for the installed C API package. + * + * This is compiled as C (not C++) on purpose: it proves the shipped headers are + * valid C and that consuming the exported CMake target does not drag C++ flags + * or unresolved C++ dependencies into a plain C project. It also reports which + * compressed storage backends the installed library provides, which is the + * capability a downstream integration has to branch on today. + */ + +#include "svs/c_api/svs_c.h" + +#include +#include + +/* Report whether a storage backend is available, without treating an absent + * proprietary backend as a failure. */ +static void report(const char* name, svs_storage_h storage, svs_error_h error) { + if (storage != NULL) { + printf("%-24s available\n", name); + svs_storage_free(storage); + return; + } + + svs_error_code_t code = svs_error_get_code(error); + const char* reason = "unavailable"; + if (code == SVS_ERROR_NOT_IMPLEMENTED) { + reason = "not built in"; + } else if (code == SVS_ERROR_UNSUPPORTED_HW) { + reason = "unsupported hardware"; + } + printf("%-24s %s (%s)\n", name, reason, svs_error_get_message(error)); +} + +int main(void) { + svs_error_h error = svs_error_create(); + if (error == NULL) { + fprintf(stderr, "failed to create an error handle\n"); + return EXIT_FAILURE; + } + + /* Simple storage is part of every build, so treat its absence as fatal: + * it is the minimum proof that the installed library actually works. */ + svs_storage_h simple = svs_storage_create_simple(SVS_DATA_TYPE_FLOAT32, error); + if (simple == NULL) { + fprintf( + stderr, "failed to create simple storage: %s\n", svs_error_get_message(error) + ); + svs_error_free(error); + return EXIT_FAILURE; + } + printf("%-24s available\n", "simple/float32"); + svs_storage_free(simple); + + report("sq/int8", svs_storage_create_sq(SVS_DATA_TYPE_INT8, error), error); + + report( + "lvq/int8", + svs_storage_create_lvq(SVS_DATA_TYPE_INT8, SVS_DATA_TYPE_VOID, error), + error + ); + + report( + "leanvec/int8", + svs_storage_create_leanvec(64, SVS_DATA_TYPE_INT8, SVS_DATA_TYPE_INT8, error), + error + ); + + svs_error_free(error); + return EXIT_SUCCESS; +} diff --git a/bindings/cpp/CMakeLists.txt b/bindings/cpp/CMakeLists.txt index 14aa58b5..57f9016b 100644 --- a/bindings/cpp/CMakeLists.txt +++ b/bindings/cpp/CMakeLists.txt @@ -58,6 +58,11 @@ else() message(STATUS "SVS runtime will be built without IVF support") endif() +# The non-LTO fallback below is correct but slower, so nothing fails and CI stays +# green. Set this where the LTO archive is the point (CI) to make the drop an error. +option(SVS_REQUIRE_LTO_ARCHIVE + "Fail instead of warn when the compiler cannot consume the LVQ/LeanVec LTO archive" OFF) + option(SVS_RUNTIME_ENABLE_LVQ_LEANVEC "Enable compilation of SVS runtime with LVQ and LeanVec support" ON) if (SVS_RUNTIME_ENABLE_LVQ_LEANVEC) message(STATUS "SVS runtime will be built with LVQ support") @@ -138,21 +143,31 @@ if (SVS_RUNTIME_ENABLE_LVQ_LEANVEC) else() # Links to LTO-enabled static library, requires GCC/G++ 11.2 if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL "11.2" AND CMAKE_CXX_COMPILER_VERSION VERSION_LESS "11.3") - set(SVS_URL "https://github.com/intel/ScalableVectorSearch/releases/download/nightly/svs-shared-library-lto-nightly-2026-05-21-1429.tar.gz" + set(SVS_URL "https://github.com/intel/ScalableVectorSearch/releases/download/v0.4.0/svs-shared-library-lto.tar.gz" CACHE STRING "URL to download SVS shared library") else() - message(WARNING + if(SVS_REQUIRE_LTO_ARCHIVE) + set(SVS_LTO_MESSAGE_LEVEL FATAL_ERROR) + else() + set(SVS_LTO_MESSAGE_LEVEL WARNING) + endif() + message(${SVS_LTO_MESSAGE_LEVEL} "Pre-built LVQ/LeanVec SVS library requires GCC/G++ v.11.2 to apply LTO optimizations." "Current compiler: ${CMAKE_CXX_COMPILER_ID} ${CMAKE_CXX_COMPILER_VERSION}" ) - set(SVS_URL "https://github.com/intel/ScalableVectorSearch/releases/download/nightly/svs-shared-library-nightly-2026-05-06-1253.tar.gz" + set(SVS_URL "https://github.com/intel/ScalableVectorSearch/releases/download/v0.4.0/svs-shared-library.tar.gz" CACHE STRING "URL to download SVS shared library") endif() include(FetchContent) + # DOWNLOAD_EXTRACT_TIMESTAMP needs CMake 3.24+; 3.22 is still around locally. + set(SVS_FETCH_EXTRA_ARGS) + if(CMAKE_VERSION VERSION_GREATER_EQUAL "3.24") + list(APPEND SVS_FETCH_EXTRA_ARGS DOWNLOAD_EXTRACT_TIMESTAMP TRUE) + endif() FetchContent_Declare( svs URL ${SVS_URL} - DOWNLOAD_EXTRACT_TIMESTAMP TRUE + ${SVS_FETCH_EXTRA_ARGS} ) FetchContent_MakeAvailable(svs) list(APPEND CMAKE_PREFIX_PATH "${svs_SOURCE_DIR}")