diff --git a/.github/workflows/01-ci-pipeline.yml b/.github/workflows/01-ci-pipeline.yml index b25b69307..c2296f501 100644 --- a/.github/workflows/01-ci-pipeline.yml +++ b/.github/workflows/01-ci-pipeline.yml @@ -93,3 +93,8 @@ jobs: name: Build & Test (Windows) needs: lint uses: ./.github/workflows/05-windows-build.yml + + build-ios: + name: Build & Test (iOS) + needs: lint + uses: ./.github/workflows/06-ios-build.yml diff --git a/.github/workflows/06-ios-build.yml b/.github/workflows/06-ios-build.yml new file mode 100644 index 000000000..4845d6203 --- /dev/null +++ b/.github/workflows/06-ios-build.yml @@ -0,0 +1,151 @@ +name: iOS Cross Build + +on: + workflow_call: + workflow_dispatch: + +permissions: + contents: read + +jobs: + build-ios: + runs-on: macos-15 + strategy: + fail-fast: false + matrix: + include: + - platform: SIMULATORARM64 + arch: arm64 + sdk: iphonesimulator + test_on_simulator: true + - platform: OS + arch: arm64 + sdk: iphoneos + test_on_simulator: false + + name: iOS (${{ matrix.platform }}) + + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + submodules: recursive + + - name: Cache host protoc build + uses: actions/cache@v5 + with: + path: build_host + key: macos-host-protoc-${{ hashFiles('src/**', 'CMakeLists.txt') }} + restore-keys: | + macos-host-protoc- + + - name: Build host protoc + run: | + if [ ! -f "build_host/bin/protoc" ]; then + cmake -S . -B build_host -DCMAKE_BUILD_TYPE=Release -DCMAKE_POLICY_VERSION_MINIMUM=3.5 + cmake --build build_host --target protoc --parallel $(sysctl -n hw.ncpu) + else + echo "Using cached host protoc" + fi + + - name: Cache iOS build + uses: actions/cache@v5 + with: + path: build_ios_${{ matrix.platform }} + key: ios-build-${{ matrix.platform }}-${{ hashFiles('src/**', 'CMakeLists.txt', 'cmake/**', 'thirdparty/**') }} + + - name: Configure and Build + run: | + git submodule foreach --recursive 'git stash --include-untracked' || true + + SDK_PATH=$(xcrun --sdk ${{ matrix.sdk }} --show-sdk-path) + NPROC=$(sysctl -n hw.ncpu) + + cmake -S . -B build_ios_${{ matrix.platform }} \ + -DCMAKE_SYSTEM_NAME=iOS \ + -DCMAKE_OSX_DEPLOYMENT_TARGET="13.0" \ + -DCMAKE_OSX_ARCHITECTURES="${{ matrix.arch }}" \ + -DCMAKE_OSX_SYSROOT="$SDK_PATH" \ + -DCMAKE_BUILD_TYPE=Release \ + -DBUILD_PYTHON_BINDINGS=OFF \ + -DBUILD_TOOLS=OFF \ + -DCMAKE_INSTALL_PREFIX="./install" \ + -DGLOBAL_CC_PROTOBUF_PROTOC="$GITHUB_WORKSPACE/build_host/bin/protoc" \ + -DIOS=ON \ + -DCMAKE_POLICY_VERSION_MINIMUM=3.5 + + cmake --build build_ios_${{ matrix.platform }} --parallel $NPROC + + - name: Build test targets + if: matrix.test_on_simulator + run: | + NPROC=$(sysctl -n hw.ncpu) + cmake --build build_ios_${{ matrix.platform }} --target unittest --parallel $NPROC + + - name: Boot iOS Simulator + if: matrix.test_on_simulator + run: | + DEVICE_ID=$(xcrun simctl list devices available -j \ + | python3 -c " + import json, sys + data = json.load(sys.stdin) + for runtime, devices in data['devices'].items(): + if 'iOS' in runtime: + for d in devices: + if 'iPhone' in d['name'] and d['isAvailable']: + print(d['udid']) + sys.exit(0) + sys.exit(1) + ") + echo "DEVICE_ID=$DEVICE_ID" >> $GITHUB_ENV + xcrun simctl boot "$DEVICE_ID" + echo "Booted simulator: $DEVICE_ID" + + - name: Run all tests on simulator + if: matrix.test_on_simulator + run: | + FAILED_TESTS="" + PASSED=0 + TOTAL=0 + + for APP in build_ios_${{ matrix.platform }}/bin/*_test.app; do + [ -d "$APP" ] || continue + TEST_NAME=$(basename "$APP" .app) + BUNDLE_ID="com.zvec.${TEST_NAME}" + TOTAL=$((TOTAL + 1)) + + echo "::group::Running ${TEST_NAME}" + xcrun simctl install "$DEVICE_ID" "$APP" + set +eo pipefail + xcrun simctl launch --console "$DEVICE_ID" "$BUNDLE_ID" 2>&1 | tee /tmp/${TEST_NAME}.log + LAUNCH_EXIT=${PIPESTATUS[0]} + set -eo pipefail + + if grep -q '\[ FAILED \]' /tmp/${TEST_NAME}.log; then + echo "::error::${TEST_NAME} has failing tests" + FAILED_TESTS="${FAILED_TESTS} ${TEST_NAME}" + elif grep -q '\[ PASSED \]' /tmp/${TEST_NAME}.log; then + PASSED=$((PASSED + 1)) + elif grep -qE 'Failed: 0$' /tmp/${TEST_NAME}.log; then + # c_api_test uses a custom test framework (not GTest) + PASSED=$((PASSED + 1)) + elif [ "$LAUNCH_EXIT" -eq 0 ]; then + echo "::warning::${TEST_NAME} exited 0 but produced no recognisable test summary" + PASSED=$((PASSED + 1)) + else + echo "::error::${TEST_NAME} exited ${LAUNCH_EXIT} with no test summary" + FAILED_TESTS="${FAILED_TESTS} ${TEST_NAME}" + fi + echo "::endgroup::" + done + + echo "Test summary: ${PASSED}/${TOTAL} passed" + if [ -n "$FAILED_TESTS" ]; then + echo "::error::Failed tests:${FAILED_TESTS}" + exit 1 + fi + + - name: Shutdown Simulator + if: matrix.test_on_simulator && always() + run: | + xcrun simctl shutdown "$DEVICE_ID" || true diff --git a/.gitignore b/.gitignore index 0827e539d..e56a7660c 100644 --- a/.gitignore +++ b/.gitignore @@ -48,4 +48,6 @@ yarn-error.log* allure-* -!build_android.sh \ No newline at end of file +!build_android.sh +!build_ios.sh + diff --git a/CMakeLists.txt b/CMakeLists.txt index ad9df2945..a33e61e99 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -33,7 +33,7 @@ else() set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -Werror=return-type") endif() -if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") +if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND NOT IOS) set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,--no-as-needed") set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,--no-as-needed") endif() @@ -47,7 +47,19 @@ message(STATUS "PROJECT_ROOT_DIR = ${PROJECT_ROOT_DIR}") include(${PROJECT_ROOT_DIR}/cmake/bazel.cmake) include(${PROJECT_ROOT_DIR}/cmake/option.cmake) -if (NOT ANDROID AND AUTO_DETECT_ARCH AND HOST_ARCH MATCHES "^(x86|x64)$") +# iOS platform detection +if(NOT ANDROID AND NOT IOS AND CMAKE_SYSTEM_NAME STREQUAL "iOS") + set(IOS TRUE) +endif() + +# iOS bundle properties for test executables +if(IOS) + set(MACOSX_BUNDLE_BUNDLE_VERSION "1") + set(MACOSX_BUNDLE_SHORT_VERSION_STRING "1.0") + set(CMAKE_MACOSX_BUNDLE_INFO_PLIST "${PROJECT_ROOT_DIR}/cmake/iOSBundleInfo.plist.in") +endif() + +if (NOT ANDROID AND NOT IOS AND AUTO_DETECT_ARCH AND HOST_ARCH MATCHES "^(x86|x64)$") setup_compiler_march_for_x86(MATH_MARCH_FLAG_SSE MATH_MARCH_FLAG_AVX2 MATH_MARCH_FLAG_AVX512 MATH_MARCH_FLAG_AVX512FP16) message(STATUS "best compiler march, sse: " ${MATH_MARCH_FLAG_SSE} ", avx2: " ${MATH_MARCH_FLAG_AVX2} ", avx512: " ${MATH_MARCH_FLAG_AVX512} ", avx512fp16: " ${MATH_MARCH_FLAG_AVX512FP16}) endif() @@ -66,7 +78,7 @@ message(STATUS "BUILD_TOOLS:${BUILD_TOOLS}") option(RABITQ_ENABLE_AVX512 "Compile RaBitQ with AVX-512 support" OFF) -if(CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|amd64|AMD64" AND NOT ANDROID) +if(CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|amd64|AMD64" AND NOT ANDROID AND NOT IOS) include(CheckCCompilerFlag) check_c_compiler_flag("-mavx2" COMPILER_SUPPORTS_AVX2) @@ -86,6 +98,10 @@ if(CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64 add_definitions(-DRABITQ_SUPPORTED=0) message(STATUS "RaBitQ support disabled - compiler does not support AVX2 or AVX-512") endif() +elseif(IOS) + set(RABITQ_SUPPORTED OFF) + add_definitions(-DRABITQ_SUPPORTED=0) + message(STATUS "RaBitQ support disabled - not supported on iOS") else() set(RABITQ_SUPPORTED OFF) add_definitions(-DRABITQ_SUPPORTED=0) diff --git a/cmake/bazel.cmake b/cmake/bazel.cmake index 925df57ed..829948229 100644 --- a/cmake/bazel.cmake +++ b/cmake/bazel.cmake @@ -301,7 +301,7 @@ ## ) ## -cmake_minimum_required(VERSION 3.1 FATAL_ERROR) +cmake_minimum_required(VERSION 3.13 FATAL_ERROR) include(CMakeParseArguments) # Using AppleClang instead of Clang (Compiler id) @@ -314,11 +314,16 @@ enable_testing() # Add unittest target if(NOT TARGET unittest) - add_custom_target( - unittest - COMMAND ${CMAKE_CTEST_COMMAND} --output-on-failure - --build-config $ - ) + if(IOS) + # iOS: build-only target; tests are run on simulator separately + add_custom_target(unittest) + else() + add_custom_target( + unittest + COMMAND ${CMAKE_CTEST_COMMAND} --output-on-failure + --build-config $ + ) + endif() endif() # Directories of target output @@ -584,9 +589,16 @@ macro(_add_library _NAME _OPTION) add_library( ${_NAME}_static STATIC ${_OPTION} $ ) - add_library( - ${_NAME} SHARED ${_OPTION} $ - ) + if(IOS) + # iOS: create the main target as static too (no shared libs on iOS) + add_library( + ${_NAME} STATIC ${_OPTION} $ + ) + else() + add_library( + ${_NAME} SHARED ${_OPTION} $ + ) + endif() add_dependencies(${_NAME} ${_NAME}_static) if(NOT MSVC) set_property(TARGET ${_NAME}_static PROPERTY OUTPUT_NAME ${_NAME}) @@ -708,7 +720,7 @@ function(_target_link_libraries _NAME) endif() if(NOT MSVC) - if(NOT ${CMAKE_SYSTEM_NAME} MATCHES "Darwin") + if(NOT ${CMAKE_SYSTEM_NAME} MATCHES "Darwin" AND NOT ${CMAKE_SYSTEM_NAME} MATCHES "iOS") list(APPEND LINK_LIBS -Wl,--whole-archive ${LIB} -Wl,--no-whole-archive) else() list(APPEND LINK_LIBS -Wl,-force_load ${LIB}) @@ -1011,6 +1023,13 @@ function(cc_binary) endif() add_executable(${CC_ARGS_NAME} ${CC_ARGS_SRCS}) + # iOS: set bundle properties for simulator/device installation + if(IOS) + set_target_properties(${CC_ARGS_NAME} PROPERTIES + MACOSX_BUNDLE_INFO_PLIST "${PROJECT_ROOT_DIR}/cmake/iOSBundleInfo.plist.in" + ) + endif() + if(CC_ARGS_PACKED) install( TARGETS ${CC_ARGS_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}" @@ -1051,8 +1070,33 @@ function(cc_test) string(REPLACE "-" "_" MACRO_PREFIX "${CC_ARGS_NAME}") list(APPEND CC_ARGS_DEFS ${MACRO_PREFIX}_VERSION="${CC_ARGS_VERSION}") endif() + # iOS: add sandbox helper to redirect CWD to writable directory + if(IOS) + list(APPEND CC_ARGS_SRCS "${PROJECT_ROOT_DIR}/tests/ios_test_sandbox.cc") + # Arrow's iOS code references CoreFoundation symbols; link Apple frameworks + list(APPEND CC_ARGS_LDFLAGS + -framework CoreFoundation + -framework CoreGraphics + -framework CoreData + -framework CoreText + -framework Security + -framework Foundation + -Wl,-U,_MallocExtension_ReleaseFreeMemory + -Wl,-U,_ProfilerStart + -Wl,-U,_ProfilerStop + -Wl,-U,_RegisterThriftProtocol + ) + endif() + add_executable(${CC_ARGS_NAME} EXCLUDE_FROM_ALL ${CC_ARGS_SRCS}) + # iOS: set bundle properties for simulator/device installation + if(IOS) + set_target_properties(${CC_ARGS_NAME} PROPERTIES + MACOSX_BUNDLE_INFO_PLIST "${PROJECT_ROOT_DIR}/cmake/iOSBundleInfo.plist.in" + ) + endif() + _cc_target_properties( NAME "${CC_ARGS_NAME}" INCS "${CC_ARGS_INCS}" @@ -2133,7 +2177,7 @@ function(_fetch_content) set( CMAKELISTS_CONTENT - "cmake_minimum_required(VERSION 3.1)\n" + "cmake_minimum_required(VERSION 3.13)\n" "project(${DL_ARGS_NAME})\n" "include(ExternalProject)\n" "ExternalProject_Add(\n" diff --git a/cmake/iOSBundleInfo.plist.in b/cmake/iOSBundleInfo.plist.in new file mode 100644 index 000000000..ac6e41e80 --- /dev/null +++ b/cmake/iOSBundleInfo.plist.in @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + English + CFBundleExecutable + ${MACOSX_BUNDLE_EXECUTABLE_NAME} + CFBundleIdentifier + com.zvec.${MACOSX_BUNDLE_EXECUTABLE_NAME} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${MACOSX_BUNDLE_EXECUTABLE_NAME} + CFBundlePackageType + APPL + CFBundleShortVersionString + ${MACOSX_BUNDLE_SHORT_VERSION_STRING} + CFBundleVersion + ${MACOSX_BUNDLE_BUNDLE_VERSION} + + diff --git a/cmake/option.cmake b/cmake/option.cmake index a4560f73d..ee97b570a 100644 --- a/cmake/option.cmake +++ b/cmake/option.cmake @@ -188,6 +188,11 @@ function(setup_compiler_march_for_x86 VAR_NAME_SSE VAR_NAME_AVX2 VAR_NAME_AVX512 endforeach() endfunction() +# iOS: Skip -march flags and OpenMP; architecture is controlled by CMAKE_OSX_ARCHITECTURES +if(IOS OR CMAKE_SYSTEM_NAME STREQUAL "iOS") + return() +endif() + if(NOT AUTO_DETECT_ARCH) if(ENABLE_NATIVE) if (NOT MSVC) diff --git a/scripts/build_ios.sh b/scripts/build_ios.sh new file mode 100755 index 000000000..4e3fb466a --- /dev/null +++ b/scripts/build_ios.sh @@ -0,0 +1,171 @@ +#!/bin/bash +set -e +CURRENT_DIR=$(pwd) + +# Platform options: OS (arm64 device), SIMULATOR64 (x86_64 sim), SIMULATORARM64 (arm64 sim) +PLATFORM=${1:-"OS"} +BUILD_TYPE=${2:-"Release"} +IOS_DEPLOYMENT_TARGET="13.0" + +# Determine architecture based on platform +case "$PLATFORM" in + "OS") + ARCH="arm64" + ;; + "SIMULATOR64") + ARCH="x86_64" + ;; + "SIMULATORARM64") + ARCH="arm64" + ;; + *) + echo "error: Unknown platform '$PLATFORM'" + echo "Usage: $0 [OS|SIMULATOR64|SIMULATORARM64] [Release|Debug]" + echo " OS - Build for iOS device (arm64)" + echo " SIMULATOR64 - Build for iOS Simulator (x86_64)" + echo " SIMULATORARM64- Build for iOS Simulator (arm64, Apple Silicon)" + exit 1 + ;; +esac + +echo "Building zvec for iOS" +echo " Platform: $PLATFORM" +echo " Architecture: $ARCH" +echo " Build Type: $BUILD_TYPE" +echo " iOS Deployment Target: $IOS_DEPLOYMENT_TARGET" + +# step1: use host env to compile protoc +echo "step1: building protoc for host..." + +git submodule foreach --recursive 'git stash --include-untracked' + +HOST_BUILD_DIR="build_host" +mkdir -p $HOST_BUILD_DIR +cd $HOST_BUILD_DIR + +cmake -DCMAKE_BUILD_TYPE="$BUILD_TYPE" .. +make -j protoc +PROTOC_EXECUTABLE=$CURRENT_DIR/$HOST_BUILD_DIR/bin/protoc +cd $CURRENT_DIR + +echo "step1: Done!!!" + +# step2: cross build zvec for iOS +echo "step2: building zvec for iOS..." + +# reset thirdparty directory +git submodule foreach --recursive 'git stash --include-untracked' + +BUILD_DIR="build_ios_${PLATFORM}" +mkdir -p $BUILD_DIR +cd $BUILD_DIR + +# Determine SDK and additional flags based on platform +if [ "$PLATFORM" = "OS" ]; then + SDK_NAME="iphoneos" +else + SDK_NAME="iphonesimulator" +fi + +SDK_PATH=$(xcrun --sdk $SDK_NAME --show-sdk-path) + +echo "configure CMake..." +cmake \ + -DCMAKE_SYSTEM_NAME=iOS \ + -DCMAKE_OSX_DEPLOYMENT_TARGET="$IOS_DEPLOYMENT_TARGET" \ + -DCMAKE_OSX_ARCHITECTURES="$ARCH" \ + -DCMAKE_OSX_SYSROOT="$SDK_PATH" \ + -DCMAKE_BUILD_TYPE="$BUILD_TYPE" \ + -DBUILD_PYTHON_BINDINGS=OFF \ + -DBUILD_TOOLS=OFF \ + -DCMAKE_INSTALL_PREFIX="./install" \ + -DGLOBAL_CC_PROTOBUF_PROTOC=$PROTOC_EXECUTABLE \ + -DIOS=ON \ + ../ + +echo "building..." +CORE_COUNT=$(sysctl -n hw.ncpu) +make -j$CORE_COUNT + +echo "step2: Done!!!" + +# step3: build and run all unit tests on simulator +if [ "$PLATFORM" != "OS" ]; then + echo "step3: building and running unit tests on simulator..." + + make -j$CORE_COUNT unittest + + # Boot simulator + DEVICE_ID=$(xcrun simctl list devices available -j \ + | python3 -c " +import json, sys +data = json.load(sys.stdin) +for runtime, devices in data['devices'].items(): + if 'iOS' in runtime: + for d in devices: + if 'iPhone' in d['name'] and d['isAvailable']: + print(d['udid']) + sys.exit(0) +sys.exit(1) +") + xcrun simctl boot "$DEVICE_ID" + echo "Booted simulator: $DEVICE_ID" + + # Run all test .app bundles + FAILED_TESTS="" + PASSED=0 + TOTAL=0 + + for APP in bin/*_test.app; do + [ -d "$APP" ] || continue + TEST_NAME=$(basename "$APP" .app) + BUNDLE_ID="com.zvec.${TEST_NAME}" + TOTAL=$((TOTAL + 1)) + + echo "--- Running ${TEST_NAME} ---" + xcrun simctl install "$DEVICE_ID" "$APP" + set +e + xcrun simctl launch --console "$DEVICE_ID" "$BUNDLE_ID" 2>&1 | tee /tmp/${TEST_NAME}.log + LAUNCH_EXIT=${PIPESTATUS[0]} + set -e + + if grep -q '\[ FAILED \]' /tmp/${TEST_NAME}.log; then + echo "FAIL: ${TEST_NAME}" + FAILED_TESTS="${FAILED_TESTS} ${TEST_NAME}" + elif grep -q '\[ PASSED \]' /tmp/${TEST_NAME}.log; then + PASSED=$((PASSED + 1)) + elif grep -qE 'Failed: 0$' /tmp/${TEST_NAME}.log; then + # c_api_test uses a custom test framework (not GTest) + PASSED=$((PASSED + 1)) + elif [ "$LAUNCH_EXIT" -eq 0 ]; then + echo "WARN: ${TEST_NAME} exited 0 but produced no recognisable test summary" + PASSED=$((PASSED + 1)) + else + echo "FAIL: ${TEST_NAME} exited ${LAUNCH_EXIT} with no test summary" + FAILED_TESTS="${FAILED_TESTS} ${TEST_NAME}" + fi + done + + # Shutdown simulator + xcrun simctl shutdown "$DEVICE_ID" || true + + echo "" + echo "Test summary: ${PASSED}/${TOTAL} passed" + if [ -n "$FAILED_TESTS" ]; then + echo "Failed tests:${FAILED_TESTS}" + exit 1 + fi + + echo "step3: Done!!!" +else + echo "Skipping tests (device build cannot run on simulator)" +fi + +echo "" +echo "Build completed successfully!" +echo "Output directory: $CURRENT_DIR/$BUILD_DIR" + +# Test On MacOS15 +# 1: xcrun simctl boot "iPhone 16" +# 2: cd $BUILD_DIR +# 3: xcrun simctl launch --console booted com.zvec.collection_test diff --git a/tests/c/CMakeLists.txt b/tests/c/CMakeLists.txt index b5e461a24..9f40ef9ca 100644 --- a/tests/c/CMakeLists.txt +++ b/tests/c/CMakeLists.txt @@ -25,4 +25,18 @@ foreach(CC_SRCS ${ALL_TEST_SRCS}) SRCS ${CC_SRCS} utils.c INCS . .. ../../src ) + + # iOS: embed the shared library inside the .app bundle so the simulator can + # find it at runtime, and patch the executable's rpath accordingly. + if(IOS AND TARGET ${CC_TARGET}) + add_custom_command(TARGET ${CC_TARGET} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E make_directory + "$/Frameworks" + COMMAND ${CMAKE_COMMAND} -E copy + "$" + "$/Frameworks/" + COMMAND install_name_tool -add_rpath "@executable_path/Frameworks" + "$/${CC_TARGET}" 2>/dev/null || true + ) + endif() endforeach() diff --git a/tests/c/c_api_test.c b/tests/c/c_api_test.c index ec13227d2..50559594b 100644 --- a/tests/c/c_api_test.c +++ b/tests/c/c_api_test.c @@ -25,6 +25,7 @@ #include #else #include +#include #include #endif @@ -53,15 +54,7 @@ static size_t get_field_count(zvec_collection_schema_t *schema) { // Cross-platform helper function to clean up temporary directories static void cleanup_temp_directory(const char *dir) { -#ifdef _WIN32 - char cmd[512]; - snprintf(cmd, sizeof(cmd), "rmdir /s /q \"%s\" 2>nul", dir); - system(cmd); -#else - char cmd[512]; - snprintf(cmd, sizeof(cmd), "rm -rf %s", dir); - system(cmd); -#endif + zvec_test_delete_dir(dir); } static int test_count = 0; @@ -5357,7 +5350,15 @@ int main(void) { system("rmdir /s /q %TEMP%\\zvec_test_* 2>nul"); system("del /q %TEMP%\\zvec_test_* 2>nul"); #else - system("rm -rf /tmp/zvec_test_*"); + { + glob_t gl; + if (glob("/tmp/zvec_test_*", 0, NULL, &gl) == 0) { + for (size_t gi = 0; gi < gl.gl_pathc; gi++) { + zvec_test_delete_dir(gl.gl_pathv[gi]); + } + globfree(&gl); + } + } #endif printf("Cleanup completed.\n\n"); diff --git a/tests/c/utils.c b/tests/c/utils.c index 6d45febcb..61c118849 100644 --- a/tests/c/utils.c +++ b/tests/c/utils.c @@ -18,6 +18,12 @@ #include #include +#ifndef _WIN32 +#include +#include +#include +#endif + // ============================================================================= // Internal Helper Functions // ============================================================================= @@ -960,10 +966,28 @@ int zvec_test_delete_dir(const char *dir_path) { int result = system(cmd); return (result == 0) ? 0 : -1; #else - // Unix/Linux/macOS platform implementation - char cmd[1024]; - snprintf(cmd, sizeof(cmd), "rm -rf \"%s\" 2>/dev/null", dir_path); - int result = system(cmd); - return (result == 0) ? 0 : -1; + // Unix/Linux/macOS/iOS: pure C recursive removal (no system() call) + struct stat st; + if (stat(dir_path, &st) != 0) { + return -1; // path does not exist + } + if (!S_ISDIR(st.st_mode)) { + return unlink(dir_path); // regular file + } + DIR *dir = opendir(dir_path); + if (!dir) { + return -1; + } + struct dirent *entry; + while ((entry = readdir(dir)) != NULL) { + if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) { + continue; + } + char child[1024]; + snprintf(child, sizeof(child), "%s/%s", dir_path, entry->d_name); + zvec_test_delete_dir(child); // recurse + } + closedir(dir); + return rmdir(dir_path); #endif } diff --git a/tests/core/algorithm/flat/flat_streamer_buffer_time_test.cc b/tests/core/algorithm/flat/flat_streamer_buffer_time_test.cc index 37d28ecd6..a76d5c573 100644 --- a/tests/core/algorithm/flat/flat_streamer_buffer_time_test.cc +++ b/tests/core/algorithm/flat/flat_streamer_buffer_time_test.cc @@ -5,6 +5,7 @@ #include #include #include +#include #include #include @@ -38,15 +39,11 @@ void FlatStreamerTest::SetUp(void) { IndexMeta(IndexMeta::DataType::DT_FP32, dim)); index_meta_ptr_->set_metric("SquaredEuclidean", 0, Params()); - char cmdBuf[100]; - snprintf(cmdBuf, 100, "rm -rf %s", dir_.c_str()); - system(cmdBuf); + zvec::ailego::FileHelper::RemovePath(dir_.c_str()); } void FlatStreamerTest::TearDown(void) { - char cmdBuf[100]; - snprintf(cmdBuf, 100, "rm -rf %s", dir_.c_str()); - system(cmdBuf); + zvec::ailego::FileHelper::RemovePath(dir_.c_str()); } TEST_F(FlatStreamerTest, TestLinearSearchMMap) { diff --git a/tests/core/algorithm/hnsw_rabitq/hnsw_rabitq_streamer_test.cc b/tests/core/algorithm/hnsw_rabitq/hnsw_rabitq_streamer_test.cc index 9bb69fca7..87c8ca204 100644 --- a/tests/core/algorithm/hnsw_rabitq/hnsw_rabitq_streamer_test.cc +++ b/tests/core/algorithm/hnsw_rabitq/hnsw_rabitq_streamer_test.cc @@ -14,8 +14,10 @@ #include "hnsw_rabitq_streamer.h" #include +#include #include #include "zvec/ailego/container/params.h" +#include "zvec/ailego/utility/file_helper.h" #include "zvec/core/framework/index_holder.h" #include "zvec/core/framework/index_streamer.h" #include "hnsw_rabitq_streamer.h" @@ -49,9 +51,7 @@ void HnswRabitqStreamerTest::SetUp(void) { } void HnswRabitqStreamerTest::TearDown(void) { - char cmdBuf[100]; - snprintf(cmdBuf, 100, "rm -rf %s", dir_.c_str()); - system(cmdBuf); + ailego::FileHelper::RemovePath(dir_.c_str()); } TEST_F(HnswRabitqStreamerTest, TestBuildAndSearch) { diff --git a/tests/core/interface/index_interface_test.cc b/tests/core/interface/index_interface_test.cc index bba80121f..4d1aefd0b 100644 --- a/tests/core/interface/index_interface_test.cc +++ b/tests/core/interface/index_interface_test.cc @@ -1348,12 +1348,11 @@ TEST(IndexInterface, Score) { TEST(IndexInterface, HNSWRabitqGeneral) { constexpr uint32_t kDimension = 64; const std::string index_name{"test_rabitq.index"}; - char cmd_buf[256]; - snprintf(cmd_buf, sizeof(cmd_buf), "rm -f %s*", index_name.c_str()); + const std::string cleanup_pattern = index_name + "*"; auto func = [&](const BaseIndexParam::Pointer ¶m, const BaseIndexQueryParam::Pointer &query_param) { - system(cmd_buf); + zvec::test_util::RemoveTestFiles(cleanup_pattern); auto index = IndexFactory::CreateAndInitIndex(*param); ASSERT_NE(nullptr, index); @@ -1376,7 +1375,7 @@ TEST(IndexInterface, HNSWRabitqGeneral) { // Fetch is meaningless for HNSWRabitq index->Close(); - system(cmd_buf); + zvec::test_util::RemoveTestFiles(cleanup_pattern); }; using namespace zvec::core; diff --git a/tests/db/crash_recovery/CMakeLists.txt b/tests/db/crash_recovery/CMakeLists.txt index 12c2423a1..32b5f5610 100644 --- a/tests/db/crash_recovery/CMakeLists.txt +++ b/tests/db/crash_recovery/CMakeLists.txt @@ -1,6 +1,12 @@ include(${PROJECT_ROOT_DIR}/cmake/bazel.cmake) include(${PROJECT_ROOT_DIR}/cmake/option.cmake) +# Crash recovery tests rely on fork()+execvp()+kill() to spawn helper processes +# and simulate crashes. These POSIX process APIs are prohibited on iOS. +if(IOS) + return() +endif() + if(APPLE) set(APPLE_FRAMEWORK_LIBS -framework CoreFoundation diff --git a/tests/ios_test_sandbox.cc b/tests/ios_test_sandbox.cc new file mode 100644 index 000000000..ca8bed2c9 --- /dev/null +++ b/tests/ios_test_sandbox.cc @@ -0,0 +1,45 @@ +// iOS test sandbox helper +// Automatically changes the working directory to a writable location +// before any test code runs. On iOS, the app bundle is read-only, +// so tests that write files need a writable CWD. + +#if defined(__APPLE__) +#include +#if TARGET_OS_IOS || TARGET_OS_SIMULATOR + +#include +#include +#include +#include + +__attribute__((constructor)) +static void ios_test_sandbox_setup() { + // 1. Raise file descriptor limit (iOS default is very low) + struct rlimit rl; + if (getrlimit(RLIMIT_NOFILE, &rl) == 0) { + rlim_t target = 65536; + if (rl.rlim_cur < target) { + rl.rlim_cur = (rl.rlim_max >= target) ? target : rl.rlim_max; + if (setrlimit(RLIMIT_NOFILE, &rl) == 0) { + fprintf(stderr, "[iOS] File descriptor limit raised to: %llu\n", + (unsigned long long)rl.rlim_cur); + } + } + } + + // 2. Change CWD to writable sandbox directory + // TMPDIR is set by iOS to the app's writable sandbox tmp directory + const char* tmpdir = getenv("TMPDIR"); + if (tmpdir && chdir(tmpdir) == 0) { + fprintf(stderr, "[iOS] Working directory set to: %s\n", tmpdir); + } else { + // Fallback: try HOME directory + const char* home = getenv("HOME"); + if (home && chdir(home) == 0) { + fprintf(stderr, "[iOS] Working directory set to: %s\n", home); + } + } +} + +#endif // TARGET_OS_IOS || TARGET_OS_SIMULATOR +#endif // __APPLE__ diff --git a/tests/test_util.h b/tests/test_util.h index 00fedd81b..e4ed8c09c 100644 --- a/tests/test_util.h +++ b/tests/test_util.h @@ -18,6 +18,13 @@ #include #include +#ifdef __APPLE__ +#include +#if TARGET_OS_IOS || TARGET_OS_SIMULATOR +#include +#endif +#endif + #ifdef _MSC_VER #include #include @@ -41,6 +48,14 @@ inline void RemoveTestFiles(const std::string &pattern) { pattern.find('?') != std::string::npos) { #ifdef _WIN32 system(("del /f /q " + pattern + " 2>NUL").c_str()); +#elif defined(__APPLE__) && (TARGET_OS_IOS || TARGET_OS_SIMULATOR) + glob_t globbuf; + if (glob(pattern.c_str(), 0, nullptr, &globbuf) == 0) { + for (size_t i = 0; i < globbuf.gl_pathc; ++i) { + ailego::FileHelper::RemovePath(globbuf.gl_pathv[i]); + } + globfree(&globbuf); + } #else system(("rm -rf " + pattern).c_str()); #endif diff --git a/thirdparty/CMakeLists.txt b/thirdparty/CMakeLists.txt index 1c552fa0b..22f06ceae 100644 --- a/thirdparty/CMakeLists.txt +++ b/thirdparty/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.1 FATAL_ERROR) +cmake_minimum_required(VERSION 3.13 FATAL_ERROR) cmake_policy(SET CMP0048 NEW) project(thirdparty) diff --git a/thirdparty/arrow/CMakeLists.txt b/thirdparty/arrow/CMakeLists.txt index 0a76184a6..e9860e1ae 100644 --- a/thirdparty/arrow/CMakeLists.txt +++ b/thirdparty/arrow/CMakeLists.txt @@ -2,6 +2,9 @@ set(ARROW_SRC_DIR ${CMAKE_CURRENT_SOURCE_DIR}/apache-arrow-21.0.0) if(ANDROID) set(ARROW_PATCH ${CMAKE_CURRENT_SOURCE_DIR}/arrow.android.patch) apply_patch_once("arrow_android_fix" "${ARROW_SRC_DIR}" "${ARROW_PATCH}") +elseif(IOS) + set(ARROW_IOS_PATCH ${CMAKE_CURRENT_SOURCE_DIR}/arrow.ios.patch) + apply_patch_once("arrow_ios_fix" "${ARROW_SRC_DIR}" "${ARROW_IOS_PATCH}") else() set(ARROW_PATCH ${CMAKE_CURRENT_SOURCE_DIR}/arrow.patch) apply_patch_once("arrow_fix" "${ARROW_SRC_DIR}" "${ARROW_PATCH}") @@ -61,6 +64,33 @@ if(ANDROID) LOG_BUILD ON LOG_INSTALL ON ) +elseif(IOS) + # Determine ARROW_CPU_FLAG based on architecture + if(CMAKE_OSX_ARCHITECTURES MATCHES "arm64") + set(IOS_ARROW_CPU_FLAG "aarch64") + elseif(CMAKE_OSX_ARCHITECTURES MATCHES "x86_64") + set(IOS_ARROW_CPU_FLAG "x86") + else() + set(IOS_ARROW_CPU_FLAG "aarch64") + endif() + # Propagate deployment target to Arrow's bundled dependencies via env var. + # Without this, sub-ExternalProjects (re2, utf8proc, etc.) pick up the SDK + # version instead of the intended deployment target, causing linker warnings. + set(_arrow_ios_env ${CONFIGURE_ENV_LIST} "IPHONEOS_DEPLOYMENT_TARGET=${CMAKE_OSX_DEPLOYMENT_TARGET}") + ExternalProject_Add( + ARROW.BUILD PREFIX arrow + SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/apache-arrow-21.0.0 + DOWNLOAD_COMMAND "" + BUILD_IN_SOURCE false + CONFIGURE_COMMAND env ${_arrow_ios_env} "${CMAKE_COMMAND}" ${CMAKE_CACHE_ARGS} -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE} -DCMAKE_DEBUG_POSTFIX= -DARROW_BUILD_SHARED=OFF -DARROW_ACERO=ON -DARROW_FILESYSTEM=ON -DARROW_DATASET=ON -DARROW_PARQUET=ON -DARROW_COMPUTE=ON -DARROW_WITH_ZLIB=OFF -DARROW_DEPENDENCY_SOURCE=BUNDLED -DARROW_MIMALLOC=OFF -DCMAKE_INSTALL_LIBDIR=lib -DCMAKE_SYSTEM_NAME=iOS -DCMAKE_OSX_DEPLOYMENT_TARGET=${CMAKE_OSX_DEPLOYMENT_TARGET} -DCMAKE_OSX_ARCHITECTURES=${CMAKE_OSX_ARCHITECTURES} -DCMAKE_OSX_SYSROOT=${CMAKE_OSX_SYSROOT} -DARROW_CPU_FLAG=${IOS_ARROW_CPU_FLAG} "/cpp" + BUILD_COMMAND env "IPHONEOS_DEPLOYMENT_TARGET=${CMAKE_OSX_DEPLOYMENT_TARGET}" "${CMAKE_COMMAND}" --build . --target all -- -j ${NPROC} + INSTALL_COMMAND "${CMAKE_COMMAND}" --install "" --prefix=${EXTERNAL_BINARY_DIR}/usr/local + BYPRODUCTS ${LIB_PARQUET} ${LIB_ARROW} ${LIB_COMPUTE} ${LIB_ACERO} ${LIB_ARROW_DEPENDS} ${LIB_ARROW_DATASET} + LOG_DOWNLOAD ON + LOG_CONFIGURE ON + LOG_BUILD ON + LOG_INSTALL ON + ) elseif (MSVC) if(ZVEC_USE_STATIC_CRT) set(_ARROW_CRT_FLAG "/MT") diff --git a/thirdparty/arrow/arrow.ios.patch b/thirdparty/arrow/arrow.ios.patch new file mode 100644 index 000000000..b84ba5d56 --- /dev/null +++ b/thirdparty/arrow/arrow.ios.patch @@ -0,0 +1,26 @@ +diff --git a/cpp/src/arrow/CMakeLists.txt b/cpp/src/arrow/CMakeLists.txt +index 1234567..abcdefg 100644 +--- a/cpp/src/arrow/CMakeLists.txt ++++ b/cpp/src/arrow/CMakeLists.txt +@@ -173,7 +173,7 @@ list(APPEND ARROW_STATIC_INSTALL_INTERFACE_LIBS ${ARROW_SYSTEM_LINK_LIBS}) + + # Need -latomic on Raspbian. + # See also: https://issues.apache.org/jira/browse/ARROW-12860 +-if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux" AND ${CMAKE_SYSTEM_PROCESSOR} MATCHES "armv7") ++if("${CMAKE_SYSTEM_NAME}" STREQUAL "Linux" AND "${CMAKE_SYSTEM_PROCESSOR}" MATCHES "armv7") + string(APPEND ARROW_PC_LIBS_PRIVATE " -latomic") + list(APPEND ARROW_SHARED_INSTALL_INTERFACE_LIBS "atomic") + list(APPEND ARROW_STATIC_INSTALL_INTERFACE_LIBS "atomic") +diff --git a/cpp/src/arrow/acero/source_node.cc b/cpp/src/arrow/acero/source_node.cc +index 0f5840676..cf68bfdcb 100644 +--- a/cpp/src/arrow/acero/source_node.cc ++++ b/cpp/src/arrow/acero/source_node.cc +@@ -407,7 +407,7 @@ struct SchemaSourceNode : public SourceNode { + struct RecordBatchReaderSourceNode : public SourceNode { + RecordBatchReaderSourceNode(ExecPlan* plan, std::shared_ptr schema, + arrow::AsyncGenerator> generator) +- : SourceNode(plan, schema, generator) {} ++ : SourceNode(plan, schema, generator, Ordering::Implicit()) {} + + static Result Make(ExecPlan* plan, std::vector inputs, + const ExecNodeOptions& options) { diff --git a/thirdparty/lz4/CMakeLists.txt b/thirdparty/lz4/CMakeLists.txt index 6b3f57b5a..5e73245f2 100644 --- a/thirdparty/lz4/CMakeLists.txt +++ b/thirdparty/lz4/CMakeLists.txt @@ -47,6 +47,42 @@ if(ANDROID) "CFLAGS=${_lz4_cflags}" ) +elseif(IOS) + # iOS cross-compilation + set(_ios_arch ${CMAKE_OSX_ARCHITECTURES}) + if(NOT _ios_arch) + set(_ios_arch "arm64") + endif() + + set(_ios_min_version ${CMAKE_OSX_DEPLOYMENT_TARGET}) + if(NOT _ios_min_version) + set(_ios_min_version "13.0") + endif() + + # Get iOS SDK path - detect from CMAKE_OSX_SYSROOT or IOS_PLATFORM + string(TOLOWER "${CMAKE_OSX_SYSROOT}" _sysroot_lower) + if(IOS_PLATFORM STREQUAL "OS" OR _sysroot_lower MATCHES "iphoneos") + set(_ios_sdk "iphoneos") + set(_ios_target_flag "-miphoneos-version-min=${_ios_min_version}") + else() + set(_ios_sdk "iphonesimulator") + set(_ios_target_flag "-mios-simulator-version-min=${_ios_min_version}") + endif() + + execute_process( + COMMAND xcrun --sdk ${_ios_sdk} --show-sdk-path + OUTPUT_VARIABLE _ios_sysroot + OUTPUT_STRIP_TRAILING_WHITESPACE + ) + + set(_lz4_cflags "-arch ${_ios_arch} -isysroot ${_ios_sysroot} ${_ios_target_flag} -fPIC") + + list(APPEND _lz4_env + "CC=${CMAKE_C_COMPILER}" + "AR=${CMAKE_AR}" + "RANLIB=${CMAKE_RANLIB}" + "CFLAGS=${_lz4_cflags}" + ) else() list(APPEND _lz4_env "CFLAGS=-fPIC") endif() diff --git a/thirdparty/rocksdb/CMakeLists.txt b/thirdparty/rocksdb/CMakeLists.txt index 334a67cdd..30b1a878b 100644 --- a/thirdparty/rocksdb/CMakeLists.txt +++ b/thirdparty/rocksdb/CMakeLists.txt @@ -2,6 +2,22 @@ set(ROCKSDB_SRC_DIR ${CMAKE_CURRENT_SOURCE_DIR}/rocksdb-8.1.1) if (ANDROID) set(ROCKSDB_ANDROID_PATCH ${CMAKE_CURRENT_SOURCE_DIR}/rocksdb.android.patch) apply_patch_once("rocksdb_android_fix" "${ROCKSDB_SRC_DIR}" "${ROCKSDB_ANDROID_PATCH}") +elseif(IOS) + # iOS-specific patch for endianness detection + set(ROCKSDB_IOS_PATCH ${CMAKE_CURRENT_SOURCE_DIR}/rocksdb.ios.patch) + apply_patch_once("rocksdb_ios_fix" "${ROCKSDB_SRC_DIR}" "${ROCKSDB_IOS_PATCH}") + # iOS-specific options + set(WITH_JNI OFF CACHE BOOL "Disable JNI for iOS" FORCE) + set(WITH_GFLAGS OFF CACHE BOOL "Disable gflags for iOS" FORCE) + # Define OS_MACOSX for iOS to disable O_DIRECT and enable Apple-specific code paths + add_definitions(-DOS_MACOSX) + # Apple Clang for arm64 always supports CRC32 intrinsics (__ARM_FEATURE_CRC32 is + # always defined), but may reject the GCC-style -march=armv8-a+crc+crypto flag + # (especially for the simulator target). Pre-set the cache variable so RocksDB's + # CHECK_C_COMPILER_FLAG is skipped and crc32c_arm64.cc is included in the build. + if(CMAKE_OSX_ARCHITECTURES MATCHES "arm64") + set(HAS_ARMV8_CRC ON CACHE INTERNAL "Force ARM CRC for iOS arm64") + endif() endif() set(ROCKSDB_BUILD_SHARED OFF CACHE BOOL "Disable install in rocksdb" FORCE) diff --git a/thirdparty/rocksdb/rocksdb.ios.patch b/thirdparty/rocksdb/rocksdb.ios.patch new file mode 100644 index 000000000..f0671f7db --- /dev/null +++ b/thirdparty/rocksdb/rocksdb.ios.patch @@ -0,0 +1,29 @@ +diff --git a/port/port_posix.h b/port/port_posix.h +index 1234567..abcdefg 100644 +--- a/port/port_posix.h ++++ b/port/port_posix.h +@@ -58,6 +58,15 @@ + #include + #include + ++// iOS/macOS endianness detection ++#if defined(__APPLE__) ++#include ++#if !defined(__BYTE_ORDER) && defined(BYTE_ORDER) ++#define __BYTE_ORDER BYTE_ORDER ++#define __LITTLE_ENDIAN LITTLE_ENDIAN ++#endif ++#endif ++ + #ifndef PLATFORM_IS_LITTLE_ENDIAN + #define PLATFORM_IS_LITTLE_ENDIAN (__BYTE_ORDER == __LITTLE_ENDIAN) + #endif +@@ -65,7 +74,7 @@ + + #if defined(OS_MACOSX) || defined(OS_SOLARIS) || defined(OS_FREEBSD) || \ + defined(OS_NETBSD) || defined(OS_OPENBSD) || defined(OS_DRAGONFLYBSD) || \ +- defined(OS_ANDROID) || defined(CYGWIN) || defined(OS_AIX) ++ defined(OS_ANDROID) || defined(CYGWIN) || defined(OS_AIX) || (defined(__APPLE__) && !defined(OS_MACOSX)) + // Use fread/fwrite/fflush on platforms without _unlocked variants + #define fread_unlocked fread + #define fwrite_unlocked fwrite