name: Reusable Build on: workflow_call: inputs: ref: description: 'Git ref to build' required: false type: string default: '' jobs: lint: name: Lint and Format Check runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: ref: ${{ inputs.ref }} - name: Setup Python uses: actions/setup-python@v5 with: python-version: '3.11' - name: Install uv uses: astral-sh/setup-uv@v4 - name: Install ruff run: | uv tool install ruff - name: Run ruff check run: | ruff check . - name: Run ruff format check run: | ruff format --check . build: needs: lint name: Build ${{ matrix.os }} Python ${{ matrix.python }} strategy: matrix: include: - os: ubuntu-22.04 python: '3.9' - os: ubuntu-22.04 python: '3.10' - os: ubuntu-22.04 python: '3.11' - os: ubuntu-22.04 python: '3.12' - os: ubuntu-22.04 python: '3.13' - os: macos-14 python: '3.9' - os: macos-14 python: '3.10' - os: macos-14 python: '3.11' - os: macos-14 python: '3.12' - os: macos-14 python: '3.13' - os: macos-13 python: '3.9' - os: macos-13 python: '3.10' - os: macos-13 python: '3.11' - os: macos-13 python: '3.12' # Note: macos-13 + Python 3.13 excluded due to PyTorch compatibility # (PyTorch 2.5+ supports Python 3.13 but not Intel Mac x86_64) runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 with: ref: ${{ inputs.ref }} submodules: recursive - name: Setup Python uses: actions/setup-python@v5 with: python-version: ${{ matrix.python }} - name: Install uv uses: astral-sh/setup-uv@v4 - name: Install system dependencies (Ubuntu) if: runner.os == 'Linux' run: | sudo apt-get update sudo apt-get install -y libomp-dev libboost-all-dev protobuf-compiler libzmq3-dev \ pkg-config libopenblas-dev patchelf libabsl-dev libaio-dev libprotobuf-dev # Install Intel MKL for DiskANN wget -q https://registrationcenter-download.intel.com/akdlm/IRC_NAS/79153e0f-74d7-45af-b8c2-258941adf58a/intel-onemkl-2025.0.0.940.sh sudo sh intel-onemkl-2025.0.0.940.sh -a --components intel.oneapi.lin.mkl.devel --action install --eula accept -s source /opt/intel/oneapi/setvars.sh echo "MKLROOT=/opt/intel/oneapi/mkl/latest" >> $GITHUB_ENV echo "LD_LIBRARY_PATH=/opt/intel/oneapi/mkl/latest/lib/intel64:$LD_LIBRARY_PATH" >> $GITHUB_ENV - name: Install system dependencies (macOS) if: runner.os == 'macOS' run: | # Don't install LLVM, use system clang for better compatibility brew install libomp boost protobuf zeromq - name: Install build dependencies run: | uv pip install --system scikit-build-core numpy swig Cython pybind11 if [[ "$RUNNER_OS" == "Linux" ]]; then uv pip install --system auditwheel else uv pip install --system delocate fi - name: Set macOS environment variables if: runner.os == 'macOS' run: | # Use brew --prefix to automatically detect Homebrew installation path HOMEBREW_PREFIX=$(brew --prefix) echo "HOMEBREW_PREFIX=${HOMEBREW_PREFIX}" >> $GITHUB_ENV echo "OpenMP_ROOT=${HOMEBREW_PREFIX}/opt/libomp" >> $GITHUB_ENV # Set CMAKE_PREFIX_PATH to let CMake find all packages automatically echo "CMAKE_PREFIX_PATH=${HOMEBREW_PREFIX}" >> $GITHUB_ENV # Set compiler flags for OpenMP (required for both backends) echo "LDFLAGS=-L${HOMEBREW_PREFIX}/opt/libomp/lib" >> $GITHUB_ENV echo "CPPFLAGS=-I${HOMEBREW_PREFIX}/opt/libomp/include" >> $GITHUB_ENV - name: Build packages run: | # Build core (platform independent) cd packages/leann-core uv build cd ../.. # Build HNSW backend cd packages/leann-backend-hnsw if [[ "${{ matrix.os }}" == macos-* ]]; then # Use system clang for better compatibility export CC=clang export CXX=clang++ export MACOSX_DEPLOYMENT_TARGET=11.0 uv build --wheel --python ${{ matrix.python }} --find-links ${GITHUB_WORKSPACE}/packages/leann-core/dist else uv build --wheel --python ${{ matrix.python }} --find-links ${GITHUB_WORKSPACE}/packages/leann-core/dist fi cd ../.. # Build DiskANN backend cd packages/leann-backend-diskann if [[ "${{ matrix.os }}" == macos-* ]]; then # Use system clang for better compatibility export CC=clang export CXX=clang++ # DiskANN requires macOS 13.3+ for sgesdd_ LAPACK function export MACOSX_DEPLOYMENT_TARGET=13.3 uv build --wheel --python ${{ matrix.python }} --find-links ${GITHUB_WORKSPACE}/packages/leann-core/dist else uv build --wheel --python ${{ matrix.python }} --find-links ${GITHUB_WORKSPACE}/packages/leann-core/dist fi cd ../.. # Build meta package (platform independent) cd packages/leann uv build cd ../.. - name: Repair wheels (Linux) if: runner.os == 'Linux' run: | # Repair HNSW wheel cd packages/leann-backend-hnsw if [ -d dist ]; then auditwheel repair dist/*.whl -w dist_repaired rm -rf dist mv dist_repaired dist fi cd ../.. # Repair DiskANN wheel cd packages/leann-backend-diskann if [ -d dist ]; then auditwheel repair dist/*.whl -w dist_repaired rm -rf dist mv dist_repaired dist fi cd ../.. - name: Repair wheels (macOS) if: runner.os == 'macOS' run: | # Repair HNSW wheel cd packages/leann-backend-hnsw if [ -d dist ]; then delocate-wheel -w dist_repaired -v dist/*.whl rm -rf dist mv dist_repaired dist fi cd ../.. # Repair DiskANN wheel cd packages/leann-backend-diskann if [ -d dist ]; then delocate-wheel -w dist_repaired -v dist/*.whl rm -rf dist mv dist_repaired dist fi cd ../.. - name: List built packages run: | echo "๐Ÿ“ฆ Built packages:" find packages/*/dist -name "*.whl" -o -name "*.tar.gz" | sort - name: Install built packages for testing run: | # Create a virtual environment with the correct Python version uv venv --python ${{ matrix.python }} source .venv/bin/activate || source .venv/Scripts/activate # Install packages using --find-links to prioritize local builds uv pip install --find-links packages/leann-core/dist --find-links packages/leann-backend-hnsw/dist --find-links packages/leann-backend-diskann/dist packages/leann-core/dist/*.whl || uv pip install --find-links packages/leann-core/dist packages/leann-core/dist/*.tar.gz uv pip install --find-links packages/leann-core/dist packages/leann-backend-hnsw/dist/*.whl uv pip install --find-links packages/leann-core/dist packages/leann-backend-diskann/dist/*.whl uv pip install packages/leann/dist/*.whl || uv pip install packages/leann/dist/*.tar.gz # Install test dependencies using extras uv pip install -e ".[test]" - name: Run tests with pytest env: CI: true # Mark as CI environment to skip memory-intensive tests OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} HF_HUB_DISABLE_SYMLINKS: 1 TOKENIZERS_PARALLELISM: false PYTORCH_ENABLE_MPS_FALLBACK: 0 # Disable MPS on macOS CI to avoid memory issues OMP_NUM_THREADS: 1 # Disable OpenMP parallelism to avoid libomp crashes MKL_NUM_THREADS: 1 # Single thread for MKL operations run: | # Activate virtual environment source .venv/bin/activate || source .venv/Scripts/activate # Add targeted debugging for pytest hangs (especially Ubuntu 22.04) if [[ "${{ matrix.os }}" == "ubuntu-22.04" ]]; then echo "๐Ÿ” [HANG DEBUG] Ubuntu 22.04 detected - enabling enhanced process monitoring" # Pre-test state echo "๐Ÿ“Š [HANG DEBUG] Pre-test process state:" ps aux | grep -E "(python|embedding|zmq)" | grep -v grep || echo "No relevant processes" echo "๐Ÿ”Œ [HANG DEBUG] Pre-test network state:" ss -tulpn | grep -E "(555[0-9]|556[0-9])" || echo "No embedding server ports" # Function to monitor processes during test monitor_processes() { while true; do sleep 30 echo "โฐ [HANG DEBUG] $(date): Process check during test execution" ps aux | grep -E "(python|pytest|embedding)" | grep -v grep | head -10 ss -tulpn | grep -E "(555[0-9]|556[0-9])" || echo "No ports" done } # Start background monitoring monitor_processes & MONITOR_PID=$! echo "๐Ÿ” [HANG DEBUG] Started background monitor (PID: $MONITOR_PID)" # Run pytest with enhanced real-time monitoring (no dependency on pytest logs) echo "๐Ÿš€ [HANG DEBUG] Starting pytest with 600s timeout and external monitoring..." # Start independent process monitor that doesn't rely on pytest output external_monitor() { local pytest_pid=$1 local start_time=$(date +%s) local last_cpu_check=0 local stable_count=0 while true; do sleep 5 current_time=$(date +%s) elapsed=$((current_time - start_time)) # Check if pytest process still exists if ! kill -0 $pytest_pid 2>/dev/null; then echo "๐Ÿ“Š [EXTERNAL] $(date): Pytest process $pytest_pid ended after ${elapsed}s" break fi # Get detailed process info ps_info=$(ps -p $pytest_pid -o pid,ppid,time,pcpu,pmem,state,comm 2>/dev/null || echo "PROCESS_GONE") if [ "$ps_info" != "PROCESS_GONE" ]; then echo "๐Ÿ“Š [EXTERNAL] $(date): Process $pytest_pid - ${ps_info}" # Extract CPU percentage and check for stability current_cpu=$(echo "$ps_info" | tail -1 | awk '{print $4}' | cut -d. -f1) if [ "$current_cpu" = "$last_cpu_check" ] && [ "$current_cpu" -lt 5 ]; then stable_count=$((stable_count + 1)) if [ $stable_count -ge 6 ]; then # 30 seconds of low CPU echo "โš ๏ธ [EXTERNAL] $(date): Process appears hung - CPU stable at ${current_cpu}% for 30s" fi else stable_count=0 fi last_cpu_check=$current_cpu # Check for zombie/stopped state state=$(echo "$ps_info" | tail -1 | awk '{print $6}') if [ "$state" = "Z" ] || [ "$state" = "T" ]; then echo "๐Ÿ’€ [EXTERNAL] $(date): Process in abnormal state: $state" fi fi # Check for orphaned Python processes orphan_count=$(ps aux | grep -E "python.*pytest" | grep -v grep | wc -l) if [ $orphan_count -gt 1 ]; then echo "๐Ÿ” [EXTERNAL] $(date): Found $orphan_count pytest-related processes" ps aux | grep -E "python.*pytest" | grep -v grep fi # Emergency timeout if [ $elapsed -gt 650 ]; then echo "๐Ÿ’ฅ [EXTERNAL] $(date): Emergency timeout reached, force killing pytest" kill -KILL $pytest_pid 2>/dev/null || true break fi done } # Run pytest in background so we can monitor it externally python -u -c "import sys, time; print(f'๐Ÿ” [REALTIME] {time.strftime(\"%H:%M:%S\")} Starting pytest...', flush=True)" timeout --preserve-status --signal=TERM --kill-after=30 600 bash -c ' echo "โ–ถ๏ธ [HANG DEBUG] Pytest starting at: $(date)" # Force unbuffered output and immediate flush stdbuf -o0 -e0 pytest tests/ -v --tb=short --maxfail=5 -x -s 2>&1 | while IFS= read -r line; do printf "%s [PYTEST] %s\n" "$(date +"%H:%M:%S")" "$line" # Force flush after each line sync done PYTEST_RESULT=${PIPESTATUS[0]} echo "โœ… [HANG DEBUG] Pytest completed at: $(date) with exit code: $PYTEST_RESULT" exit $PYTEST_RESULT ' & PYTEST_PID=$! echo "๐Ÿ” [HANG DEBUG] Pytest started with PID: $PYTEST_PID" # Start external monitoring external_monitor $PYTEST_PID & EXTERNAL_MONITOR_PID=$! # Wait for pytest to complete wait $PYTEST_PID PYTEST_EXIT=$? echo "๐Ÿ [HANG DEBUG] Pytest process ended with exit code: $PYTEST_EXIT" # Stop external monitor kill $EXTERNAL_MONITOR_PID 2>/dev/null || true # Final cleanup check echo "๐Ÿงน [HANG DEBUG] Final cleanup check..." REMAINING_PROCS=$(ps aux | grep -E "python.*pytest" | grep -v grep | wc -l) if [ $REMAINING_PROCS -gt 0 ]; then echo "โš ๏ธ [HANG DEBUG] Found $REMAINING_PROCS remaining pytest processes after completion" ps aux | grep -E "python.*pytest" | grep -v grep echo "๐Ÿ’€ [HANG DEBUG] Force killing remaining processes..." ps aux | grep -E "python.*pytest" | grep -v grep | awk "{print \$2}" | xargs -r kill -KILL else echo "โœ… [HANG DEBUG] No remaining pytest processes found" fi PYTEST_EXIT=$? # Stop background monitoring kill $MONITOR_PID 2>/dev/null || true echo "๐Ÿ”š [HANG DEBUG] Pytest exit code: $PYTEST_EXIT" if [ $PYTEST_EXIT -eq 124 ]; then echo "โš ๏ธ [HANG DEBUG] TIMEOUT! Pytest hung for >600s" echo "๐Ÿ” [HANG DEBUG] Final process state:" ps aux | grep -E "(python|pytest|embedding)" | grep -v grep echo "๐Ÿ” [HANG DEBUG] Final network state:" ss -tulpn | grep -E "(555[0-9]|556[0-9])" || echo "No ports" echo "๐Ÿ’€ [HANG DEBUG] Killing remaining processes..." pkill -TERM -f "pytest\|embedding_server\|zmq" || true sleep 3 pkill -KILL -f "pytest\|embedding_server\|zmq" || true fi exit $PYTEST_EXIT else # For non-Ubuntu or non-22.04, run normally echo "๐Ÿš€ [HANG DEBUG] Running tests on ${{ matrix.os }} (normal mode)" pytest tests/ -v --tb=short fi - name: Run sanity checks (optional) run: | # Activate virtual environment source .venv/bin/activate || source .venv/Scripts/activate # Run distance function tests if available if [ -f test/sanity_checks/test_distance_functions.py ]; then echo "Running distance function sanity checks..." python test/sanity_checks/test_distance_functions.py || echo "โš ๏ธ Distance function test failed, continuing..." fi - name: Upload artifacts uses: actions/upload-artifact@v4 with: name: packages-${{ matrix.os }}-py${{ matrix.python }} path: packages/*/dist/