Build Wheels for Sequenzo #268
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # GitHub Actions workflow for Sequenzo package | |
| name: Build Wheels for Sequenzo | |
| on: | |
| push: | |
| tags: | |
| - "v*" | |
| pull_request: | |
| branches: [ main ] | |
| workflow_dispatch: | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| jobs: | |
| build: | |
| name: Build ${{ matrix.os }} Py${{ matrix.python-version }} | |
| runs-on: ${{ matrix.os }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| # Linux 配置 | |
| - os: ubuntu-latest | |
| python-version: '3.9' | |
| python-version-nodot: '39' | |
| - os: ubuntu-latest | |
| python-version: '3.10' | |
| python-version-nodot: '310' | |
| - os: ubuntu-latest | |
| python-version: '3.11' | |
| python-version-nodot: '311' | |
| - os: ubuntu-latest | |
| python-version: '3.12' | |
| python-version-nodot: '312' | |
| # Windows 配置 | |
| - os: windows-latest | |
| python-version: '3.9' | |
| python-version-nodot: '39' | |
| - os: windows-latest | |
| python-version: '3.10' | |
| python-version-nodot: '310' | |
| - os: windows-latest | |
| python-version: '3.11' | |
| python-version-nodot: '311' | |
| - os: windows-latest | |
| python-version: '3.12' | |
| python-version-nodot: '312' | |
| # macOS x86_64 | |
| - os: macos-15-intel # 使用 Intel Mac 运行器 | |
| python-version: '3.9' | |
| python-version-nodot: '39' | |
| macos-arch: 'x86_64' | |
| macos-group: intel | |
| - os: macos-15-intel | |
| python-version: '3.10' | |
| python-version-nodot: '310' | |
| macos-arch: 'x86_64' | |
| macos-group: intel | |
| - os: macos-15-intel | |
| python-version: '3.11' | |
| python-version-nodot: '311' | |
| macos-arch: 'x86_64' | |
| macos-group: intel | |
| - os: macos-15-intel | |
| python-version: '3.12' | |
| python-version-nodot: '312' | |
| macos-arch: 'x86_64' | |
| macos-group: intel | |
| # macOS arm64 | |
| - os: macos-latest # 使用 Apple Silicon Mac 运行器 | |
| python-version: '3.9' | |
| python-version-nodot: '39' | |
| macos-arch: 'arm64' | |
| macos-group: apple-silicon | |
| - os: macos-latest | |
| python-version: '3.10' | |
| python-version-nodot: '310' | |
| macos-arch: 'arm64' | |
| macos-group: apple-silicon | |
| - os: macos-latest | |
| python-version: '3.11' | |
| python-version-nodot: '311' | |
| macos-arch: 'arm64' | |
| macos-group: apple-silicon | |
| - os: macos-latest | |
| python-version: '3.12' | |
| python-version-nodot: '312' | |
| macos-arch: 'arm64' | |
| macos-group: apple-silicon | |
| steps: | |
| - name: Checkout source with submodules | |
| uses: actions/checkout@v4 | |
| with: | |
| submodules: recursive | |
| - name: Set up Python | |
| uses: actions/setup-python@v4 | |
| with: | |
| python-version: ${{ matrix.python-version }} | |
| - name: Ensure setuptools/wheel for macOS | |
| if: runner.os == 'macOS' | |
| run: python -m pip install --upgrade pip setuptools wheel cibuildwheel | |
| # Add OpenMP support | |
| - name: Install OpenMP (macOS) | |
| if: runner.os == 'macOS' | |
| run: | | |
| # 检测架构并设置正确的 Homebrew 命令 | |
| if [ "${{ matrix.macos-arch }}" == "x86_64" ]; then | |
| # Intel macOS - 强制使用 x86_64 架构安装 | |
| echo "Installing x86_64 libomp on Intel macOS" | |
| # 使用 arch 命令强制 x86_64 架构 | |
| arch -x86_64 /bin/bash -c " | |
| # 安装或更新 x86_64 Homebrew | |
| if [ ! -f /usr/local/bin/brew ]; then | |
| echo 'Installing x86_64 Homebrew...' | |
| /bin/bash -c \"\$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\" | |
| else | |
| echo 'x86_64 Homebrew already installed' | |
| fi | |
| # 使用 x86_64 Homebrew 安装 libomp | |
| /usr/local/bin/brew install libomp | |
| # 获取 libomp 路径 | |
| LIBOMP_PREFIX=\$(/usr/local/bin/brew --prefix libomp) | |
| echo \"x86_64 libomp installed at: \$LIBOMP_PREFIX\" | |
| echo \"LIBOMP_PREFIX=\$LIBOMP_PREFIX\" > /tmp/libomp_prefix.txt | |
| " | |
| # 读取 x86_64 libomp 路径 | |
| LIBOMP_PREFIX=$(cat /tmp/libomp_prefix.txt | grep LIBOMP_PREFIX | cut -d= -f2) | |
| echo "Final LIBOMP_PREFIX for x86_64: $LIBOMP_PREFIX" | |
| else | |
| # Apple Silicon macOS - 使用原生架构 | |
| echo "Installing native libomp on Apple Silicon macOS" | |
| brew install libomp | |
| LIBOMP_PREFIX=$(brew --prefix libomp) | |
| echo "Final LIBOMP_PREFIX for arm64: $LIBOMP_PREFIX" | |
| fi | |
| # 验证 libomp 架构 | |
| echo "libomp prefix: $LIBOMP_PREFIX" | |
| if [ -f "$LIBOMP_PREFIX/lib/libomp.dylib" ]; then | |
| echo "libomp.dylib architecture:" | |
| file "$LIBOMP_PREFIX/lib/libomp.dylib" || echo "Cannot check architecture" | |
| fi | |
| # 设置环境变量 - 使用正确的格式 | |
| { | |
| echo "CC=clang" | |
| echo "CXX=clang++" | |
| echo "LDFLAGS=-L$LIBOMP_PREFIX/lib -Wl,-rpath,@loader_path/../.dylibs" | |
| echo "CPPFLAGS=-I$LIBOMP_PREFIX/include" | |
| echo "SEQUENZO_ENABLE_OPENMP=1" | |
| echo "LIBOMP_PATH=$LIBOMP_PREFIX/lib" | |
| } >> "$GITHUB_ENV" | |
| - name: Install OpenMP (Linux) | |
| if: runner.os == 'Linux' | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y libomp-dev | |
| echo "SEQUENZO_ENABLE_OPENMP=1" >> $GITHUB_ENV | |
| - name: Setup MSVC with OpenMP (Windows) | |
| if: runner.os == 'Windows' | |
| uses: ilammy/msvc-dev-cmd@v1 | |
| with: | |
| arch: x64 | |
| - name: Enable OpenMP (Windows) | |
| if: runner.os == 'Windows' | |
| run: | | |
| echo "SEQUENZO_ENABLE_OPENMP=1" >> $env:GITHUB_ENV | |
| - name: Set Windows encoding (Windows only) | |
| if: runner.os == 'Windows' | |
| run: | | |
| echo "PYTHONIOENCODING=utf-8" >> $env:GITHUB_ENV | |
| echo "Setting Python encoding to UTF-8 for Windows" | |
| # Install dependencies with fallback strategy | |
| - name: Install dependencies | |
| run: | | |
| python -m pip install --upgrade pip setuptools wheel cibuildwheel | |
| # Use oldest-supported-numpy for builds to ensure forward compatibility | |
| pip install oldest-supported-numpy || pip install "numpy>=1.21.0" | |
| pip install --only-binary=all "scipy<1.16" || echo "scipy will be installed in cibuildwheel" | |
| pip install --prefer-binary Cython pybind11 build "cibuildwheel==2.23.3" twine | |
| shell: bash | |
| - name: Clean before cibuildwheel | |
| run: | | |
| echo "=== Cleaning build artifacts before cibuildwheel ===" | |
| # 彻底清理所有构建产物 | |
| rm -rf build/ dist/ *.egg-info | |
| find . -name "*.so" -delete | |
| find . -name "*.c" -delete | |
| find . -name "__pycache__" -type d -exec rm -rf {} + 2>/dev/null || true | |
| # 检查是否还有残留的共享库文件 | |
| echo "Remaining shared libraries:" | |
| find . -name "*.so" | head -10 || echo "No shared libraries found" | |
| shell: bash | |
| # 修复 NumPy 2.0 和 pybind11 兼容性问题 | |
| - name: Install dependencies with NumPy 2.0 compatibility | |
| run: | | |
| python -m pip install --upgrade pip setuptools wheel | |
| # 为 Python 3.12 特别处理 NumPy 兼容性 | |
| if [ "${{ matrix.python-version }}" == "3.12" ]; then | |
| echo "Installing NumPy 2.0 compatible dependencies for Python 3.12" | |
| pip install "numpy>=2.0.0" "pybind11>=2.12.0" | |
| fi | |
| shell: bash | |
| - name: Build Cython extensions | |
| run: | | |
| echo "=== Building Cython extensions ===" | |
| # 安装 Cython 和构建依赖 | |
| python -m pip install Cython numpy | |
| # 检查 Cython 文件是否存在 | |
| echo "Checking for Cython files:" | |
| find . -name "*.pyx" -o -name "*.pxd" | head -10 | |
| # Linux 特定修复:设置正确的包含路径 | |
| if [ "$RUNNER_OS" == "Linux" ]; then | |
| echo "=== Applying Linux-specific build fixes ===" | |
| # 确保在项目根目录 | |
| echo "Current directory: $(pwd)" | |
| # 验证源文件路径 | |
| SRC_DIR="sequenzo/dissimilarity_measures/src" | |
| if [ -d "$SRC_DIR" ]; then | |
| echo "Source directory exists: $SRC_DIR" | |
| echo "Contents:" | |
| ls -la "$SRC_DIR/" | head -10 | |
| else | |
| echo "[ERROR] Source directory not found: $SRC_DIR" | |
| # 尝试创建或查找正确的路径 | |
| find . -name "OMdistance.cpp" -type f 2>/dev/null | head -5 | |
| fi | |
| # 设置环境变量以确保构建系统找到文件 | |
| export PYTHONPATH=$(pwd):$PYTHONPATH | |
| echo "PYTHONPATH set to: $PYTHONPATH" | |
| fi | |
| # 尝试构建 Cython 扩展 | |
| echo "Attempting to build Cython extensions in place..." | |
| python setup.py build_ext --inplace | |
| # 检查生成的共享库 | |
| echo "Generated shared libraries:" | |
| find . -name "*.so" -o -name "*.pyd" -o -name "*.dylib" | head -20 | |
| # 验证特定模块 - 使用 ASCII 字符避免编码问题 | |
| echo "Checking for specific modules:" | |
| python -c " | |
| import os | |
| import glob | |
| import sys | |
| # 设置标准输出编码为 UTF-8 以避免 Windows 编码问题 | |
| if sys.platform == 'win32': | |
| import io | |
| sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace') | |
| expected_modules = [ | |
| 'get_sm_trate_substitution_cost_matrix', | |
| 'seqconc', | |
| 'seqdss', | |
| 'seqdur', | |
| 'seqlength' | |
| ] | |
| for module in expected_modules: | |
| module_path = f'sequenzo/dissimilarity_measures/utils/{module}' | |
| # 检查 .pyx 源文件 | |
| pyx_file = f'{module_path}.pyx' | |
| if os.path.exists(pyx_file): | |
| print(f'[OK] Found source: {pyx_file}') | |
| else: | |
| print(f'[FAIL] Missing source: {pyx_file}') | |
| # 检查编译后的文件 | |
| so_pattern = f'**/{module}.*.so' | |
| pyd_pattern = f'**/{module}.*.pyd' | |
| dylib_pattern = f'**/{module}.*.dylib' | |
| compiled_files = glob.glob(so_pattern, recursive=True) + \ | |
| glob.glob(pyd_pattern, recursive=True) + \ | |
| glob.glob(dylib_pattern, recursive=True) | |
| if compiled_files: | |
| print(f'[OK] Found compiled: {compiled_files[0]}') | |
| else: | |
| print(f'[FAIL] Not compiled: {module}') | |
| " | |
| shell: bash | |
| - name: Build wheels with cibuildwheel | |
| run: python -m cibuildwheel --output-dir dist | |
| env: | |
| CIBW_BUILD: "cp${{ matrix.python-version-nodot }}-*" # 只构建当前 Python 版本的轮子 (e.g., cp39-*, cp310-*) | |
| CIBW_SKIP: "*-musllinux* pp*" # 跳过 musllinux 和 PyPy | |
| CIBW_ARCHS_WINDOWS: "AMD64" | |
| CIBW_ARCHS_LINUX: "x86_64" | |
| CIBW_ARCHS_MACOS: "${{ matrix.macos-arch }}" | |
| # Alternative: Use separate architecture wheels if universal2 causes issues | |
| # CIBW_ARCHS_MACOS: "x86_64 arm64" # Build separate wheels for each architecture | |
| # 或者根据架构设置不同的目标 | |
| CIBW_TARGET_OSX_x86_64: "10.15" | |
| CIBW_TARGET_OSX_arm64: "11.0" | |
| CIBW_BUILD_VERBOSITY: "3" | |
| # Environment variables | |
| CIBW_ENVIRONMENT_LINUX: SEQUENZO_ENABLE_OPENMP=1 | |
| CIBW_ENVIRONMENT_WINDOWS: SEQUENZO_ENABLE_OPENMP=1 DISTUTILS_USE_SDK=1 | |
| # macOS: Set OpenMP environment with proper library paths and rpath for bundling | |
| CIBW_ENVIRONMENT_MACOS: > | |
| SEQUENZO_ENABLE_OPENMP=1 | |
| LDFLAGS="-L$LIBOMP_PATH -Wl,-rpath,@loader_path/../.dylibs" | |
| CPPFLAGS="-I$LIBOMP_PATH/../include" | |
| # macOS: ensure libomp is available and properly linked | |
| CIBW_BEFORE_BUILD_MACOS: | | |
| echo "Installing build dependencies for macOS" | |
| python -m pip install --upgrade pip | |
| CURRENT_ARCH=$(uname -m) | |
| echo "Building for architecture: $CURRENT_ARCH" | |
| # 安装 delocate | |
| echo "Installing delocate for wheel repair..." | |
| pip install delocate | |
| # 验证 libomp | |
| if [ -d "$LIBOMP_PATH" ]; then | |
| echo "[OK] libomp found at: $LIBOMP_PATH" | |
| if [ -f "$LIBOMP_PATH/libomp.dylib" ]; then | |
| echo "libomp.dylib architecture:" | |
| file "$LIBOMP_PATH/libomp.dylib" | |
| fi | |
| else | |
| echo "[WARNING] libomp not found at $LIBOMP_PATH" | |
| fi | |
| # macOS: Use delocate to bundle libomp into the wheel | |
| CIBW_REPAIR_WHEEL_COMMAND_MACOS: | | |
| echo "=== Repairing macOS wheel ===" | |
| echo "Current architecture: $(uname -m)" | |
| echo "Wheel to repair: {wheel}" | |
| # 使用 delocate 修复 | |
| delocate-wheel -w {dest_dir} -v {wheel} | |
| if [ $? -eq 0 ]; then | |
| echo "[SUCCESS] Wheel repaired successfully" | |
| else | |
| echo "[WARNING] delocate failed, copying wheel as-is" | |
| mkdir -p {dest_dir} | |
| cp {wheel} {dest_dir}/ | |
| fi | |
| # 验证修复结果 | |
| if ls {dest_dir}/*.whl 1> /dev/null 2>&1; then | |
| REPAIRED_WHEEL=$(ls {dest_dir}/*.whl | head -1) | |
| echo "Repaired wheel: $REPAIRED_WHEEL" | |
| delocate-listdeps "$REPAIRED_WHEEL" 2>&1 || echo "Dependency check completed" | |
| fi | |
| # Linux: minimal build dependencies (skip R/rpy2 - they're optional) | |
| CIBW_BEFORE_BUILD_LINUX: | | |
| echo "Installing minimal build dependencies for Linux" | |
| python -m pip install --upgrade pip | |
| echo "Skipping R/rpy2 installation - they are optional dependencies" | |
| # 修复 Linux 构建问题 | |
| echo "=== Preparing clean Linux build ===" | |
| python -m pip install --upgrade pip | |
| echo "Ensuring clean build environment..." | |
| # 再次清理,确保没有残留的构建文件 | |
| find . -name "*.so" -delete 2>/dev/null || true | |
| find . -name "*.c" -delete 2>/dev/null || true | |
| rm -rf build/ dist/ *.egg-info | |
| echo "Clean build environment ready" | |
| # 使用绝对路径验证源文件 | |
| echo "Current working directory: $(pwd)" | |
| echo "Absolute path verification:" | |
| if [ -f "$(pwd)/sequenzo/dissimilarity_measures/src/module.cpp" ]; then | |
| echo "[SUCCESS] module.cpp found at absolute path: $(pwd)/sequenzo/dissimilarity_measures/src/module.cpp" | |
| else | |
| echo "[ERROR] module.cpp NOT FOUND at absolute path: $(pwd)/sequenzo/dissimilarity_measures/src/module.cpp" | |
| echo "Available files in src directory:" | |
| ls -la "$(pwd)/sequenzo/dissimilarity_measures/src/" 2>/dev/null || echo "Cannot list src directory" | |
| fi | |
| # 设置 Linux 修复策略 | |
| CIBW_REPAIR_WHEEL_COMMAND_LINUX: | | |
| echo "=== Attempting wheel repair ===" | |
| echo "Wheel to repair: {wheel}" | |
| # 尝试修复,如果失败则跳过修复 | |
| auditwheel repair -w {dest_dir} {wheel} || { | |
| echo "[WARNING] auditwheel repair failed, copying wheel as-is" | |
| mkdir -p {dest_dir} | |
| cp {wheel} {dest_dir}/ | |
| } | |
| # 检查修复结果 | |
| if ls {dest_dir}/*.whl 1> /dev/null 2>&1; then | |
| REPAIRED_WHEEL=$(ls {dest_dir}/*.whl | head -1) | |
| echo "Final wheel: $REPAIRED_WHEEL" | |
| auditwheel show "$REPAIRED_WHEEL" || echo "Cannot show wheel info" | |
| fi | |
| # Windows: setup MSVC environment | |
| CIBW_BEFORE_BUILD_WINDOWS: | | |
| echo "Setting up MSVC environment" | |
| call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvars64.bat" | |
| set PATH="C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Tools\MSVC\14.44.35207\bin\HostX64\x64";%PATH% | |
| echo %PATH% | |
| echo "Using minimal build dependencies" | |
| python -m pip install --upgrade pip | |
| # Universal dependency installation (fallback) | |
| CIBW_BEFORE_BUILD: > | |
| echo "Using setup.py dependencies only" | |
| # Test command: use heredoc to keep the entire script in one shell command (fix Linux quoting) | |
| CIBW_TEST_COMMAND_MACOS: | | |
| # 根据架构设置正确的库路径 | |
| CURRENT_ARCH=$(uname -m) | |
| echo "=== Testing on $CURRENT_ARCH ===" | |
| # 检测架构并设置正确的 libomp 路径 | |
| if [ "$CURRENT_ARCH" == "x86_64" ]; then | |
| # Intel macOS | |
| EXPECTED_LIBOMP_PATH="/usr/local/opt/libomp/lib" | |
| else | |
| # Apple Silicon macOS | |
| EXPECTED_LIBOMP_PATH="/opt/homebrew/opt/libomp/lib" | |
| fi | |
| # 如果预期路径存在,使用它;否则使用环境变量中的路径 | |
| if [ -d "$EXPECTED_LIBOMP_PATH" ]; then | |
| ACTUAL_LIBOMP_PATH="$EXPECTED_LIBOMP_PATH" | |
| echo "Using architecture-specific libomp path: $ACTUAL_LIBOMP_PATH" | |
| else | |
| ACTUAL_LIBOMP_PATH="$LIBOMP_PATH" | |
| echo "Using environment LIBOMP_PATH: $ACTUAL_LIBOMP_PATH" | |
| fi | |
| # 设置库路径 | |
| export DYLD_LIBRARY_PATH="$ACTUAL_LIBOMP_PATH:$DYLD_LIBRARY_PATH" | |
| export DYLD_FALLBACK_LIBRARY_PATH="$ACTUAL_LIBOMP_PATH:$DYLD_FALLBACK_LIBRARY_PATH" | |
| echo "Python version: $(python --version)" | |
| echo "Final DYLD_LIBRARY_PATH: $DYLD_LIBRARY_PATH" | |
| # 验证 libomp 文件 | |
| if [ -d "$ACTUAL_LIBOMP_PATH" ]; then | |
| echo "libomp directory contents:" | |
| ls -la "$ACTUAL_LIBOMP_PATH" || echo "Cannot list libomp directory" | |
| if [ -f "$ACTUAL_LIBOMP_PATH/libomp.dylib" ]; then | |
| echo "libomp.dylib architecture:" | |
| file "$ACTUAL_LIBOMP_PATH/libomp.dylib" || echo "Cannot check libomp architecture" | |
| else | |
| echo "WARNING: libomp.dylib not found at $ACTUAL_LIBOMP_PATH/libomp.dylib" | |
| fi | |
| else | |
| echo "ERROR: libomp directory not found: $ACTUAL_LIBOMP_PATH" | |
| fi | |
| # Check test environment | |
| echo "Test architecture: $(uname -m)" | |
| echo "Python version: $(python --version)" | |
| # Find and inspect the installed wheel | |
| echo "" | |
| echo "=== Checking installed sequenzo package ===" | |
| python -c "import sequenzo, os; print(f'Sequenzo location: {sequenzo.__file__}')" || true | |
| # Try to find bundled dylibs | |
| python -c " | |
| import sequenzo, os, glob | |
| pkg_dir = os.path.dirname(sequenzo.__file__) | |
| dylibs_dir = os.path.join(pkg_dir, '.dylibs') | |
| if os.path.exists(dylibs_dir): | |
| print(f'[OK] .dylibs directory found: {dylibs_dir}') | |
| libs = glob.glob(os.path.join(dylibs_dir, '*.dylib')) | |
| if libs: | |
| print(f'Bundled libraries: {libs}') | |
| else: | |
| print('[WARNING] .dylibs directory is empty') | |
| else: | |
| print('[WARNING] .dylibs directory not found - wheel may not be properly bundled') | |
| " || true | |
| echo "" | |
| echo "=== Testing Sequenzo import ===" | |
| python - <<'PY' | |
| import importlib.util | |
| import sys | |
| import os | |
| import numpy as np | |
| # Try to import sequenzo | |
| try: | |
| import sequenzo | |
| print('[OK] Sequenzo import successful') | |
| except ImportError as e: | |
| print(f'[ERROR] Failed to import sequenzo: {e}') | |
| sys.exit(1) | |
| # Check C++ clustering extensions (clustering) | |
| mod = importlib.util.find_spec('sequenzo.clustering.clustering_c_code') | |
| if mod: | |
| print('[OK] Clustering C++ extensions module spec found') | |
| try: | |
| import sequenzo.clustering.clustering_c_code | |
| print('[OK] Clustering C++ extensions loaded successfully') | |
| except ImportError as e: | |
| print(f'[WARNING] Clustering C++ extensions found but failed to load: {e}') | |
| if 'kmpc' in str(e) or 'omp' in str(e): | |
| print('[INFO] This appears to be an OpenMP linking issue') | |
| print('[INFO] The wheel may need libomp to be installed on the system') | |
| else: | |
| print('[WARNING] Clustering C++ extensions not found') | |
| # Check dissimilarity measures | |
| try: | |
| from sequenzo.dissimilarity_measures import c_code | |
| print('[OK] Dissimilarity measures C++ extensions (c_code) loaded successfully') | |
| # Test specific C++ classes | |
| for class_name in ['OMdistance', 'OMspellDistance', 'DHDdistance', 'LCPdistance', 'dist2matrix']: | |
| if hasattr(c_code, class_name): | |
| print(f'[OK] {class_name} class available') | |
| except ImportError as e: | |
| print(f'[WARNING] Dissimilarity measures C++ extensions (c_code) failed to load: {e}') | |
| # Check Cython modules | |
| cython_modules = [ | |
| 'get_sm_trate_substitution_cost_matrix', | |
| 'seqconc', | |
| 'seqdss', | |
| 'seqdur', | |
| 'seqlength' | |
| ] | |
| for module_name in cython_modules: | |
| try: | |
| module = __import__(f'sequenzo.dissimilarity_measures.utils.{module_name}', fromlist=[module_name]) | |
| print(f'[OK] Cython module {module_name} loaded successfully') | |
| except ImportError as e: | |
| print(f'[WARNING] Cython module {module_name} failed to load: {e}') | |
| try: | |
| from sequenzo.dissimilarity_measures.utils import ( | |
| get_sm_trate_substitution_cost_matrix, seqconc, seqdss, seqdur, seqlength | |
| ) | |
| print('[OK] All utility functions imported successfully') | |
| except ImportError as e: | |
| print(f'[WARNING] Utility functions failed to load: {e}') | |
| # Check sequenzo_fastcluster module | |
| print('') | |
| print('=== Testing sequenzo_fastcluster module ===') | |
| try: | |
| from sequenzo.clustering.sequenzo_fastcluster.fastcluster import linkage, linkage_vector, single, complete, average | |
| print('[OK] sequenzo_fastcluster wrapper imported successfully') | |
| # Test linkage function with small dataset | |
| from scipy.spatial.distance import pdist | |
| test_data = np.array([[1, 2], [1, 4], [1, 0], [4, 2], [4, 4], [4, 0]]) | |
| dist_matrix = pdist(test_data) | |
| # Test different linkage methods | |
| for method in ['single', 'complete', 'average']: | |
| try: | |
| Z = linkage(dist_matrix, method=method) | |
| assert Z.shape == (len(test_data) - 1, 4) | |
| assert np.all(np.isfinite(Z)) | |
| print(f'[OK] linkage({method}) works correctly') | |
| except Exception as e: | |
| print(f'[WARNING] linkage({method}) failed: {e}') | |
| # Test linkage_vector | |
| try: | |
| Z_vec = linkage_vector(test_data, method='complete', metric='euclidean') | |
| assert Z_vec.shape == (len(test_data) - 1, 4) | |
| assert np.all(np.isfinite(Z_vec)) | |
| print('[OK] linkage_vector works correctly') | |
| except Exception as e: | |
| print(f'[WARNING] linkage_vector failed: {e}') | |
| except ImportError as e: | |
| print(f'[WARNING] sequenzo_fastcluster module not available: {e}') | |
| print('[INFO] This module provides optimized hierarchical clustering') | |
| print('') | |
| print('[INFO] Test completed - some warnings are expected for optional dependencies') | |
| PY | |
| # Linux/other platforms test command (original) | |
| CIBW_TEST_COMMAND: | | |
| python - <<'PY' | |
| import importlib.util | |
| import sys | |
| import numpy as np | |
| import sequenzo | |
| print('Sequenzo import successful') | |
| mod = importlib.util.find_spec('sequenzo.clustering.clustering_c_code') | |
| print('Clustering C++ extensions loaded' if mod else 'WARNING: Clustering C++ extensions not found') | |
| try: | |
| from sequenzo.dissimilarity_measures import c_code | |
| print('Dissimilarity measures C++ extensions (c_code) loaded successfully') | |
| # Test specific C++ classes | |
| try: | |
| # Test if classes are available (don't instantiate to avoid errors) | |
| if hasattr(c_code, 'OMdistance'): | |
| print('[OK] OMdistance class available') | |
| if hasattr(c_code, 'OMspellDistance'): | |
| print('[OK] OMspellDistance class available') | |
| if hasattr(c_code, 'DHDdistance'): | |
| print('[OK] DHDdistance class available') | |
| if hasattr(c_code, 'LCPdistance'): | |
| print('[OK] LCPdistance class available') | |
| if hasattr(c_code, 'dist2matrix'): | |
| print('[OK] dist2matrix class available') | |
| except Exception as e: | |
| print(f'WARNING: Some C++ classes may not be available: {e}') | |
| except ImportError as e: | |
| print(f'ERROR: Dissimilarity measures C++ extensions (c_code) failed to load: {e}') | |
| cython_modules = [ | |
| 'get_sm_trate_substitution_cost_matrix', | |
| 'seqconc', | |
| 'seqdss', | |
| 'seqdur', | |
| 'seqlength' | |
| ] | |
| for module_name in cython_modules: | |
| try: | |
| module = __import__(f'sequenzo.dissimilarity_measures.utils.{module_name}', fromlist=[module_name]) | |
| print(f'[OK] Cython module {module_name} loaded successfully') | |
| except ImportError as e: | |
| print(f'ERROR: Cython module {module_name} failed to load: {e}') | |
| try: | |
| from sequenzo.dissimilarity_measures.utils import ( | |
| get_sm_trate_substitution_cost_matrix, seqconc, seqdss, seqdur, seqlength | |
| ) | |
| print('[OK] All utility functions imported successfully') | |
| except ImportError as e: | |
| print(f'ERROR: Utility functions failed to load: {e}') | |
| # Check sequenzo_fastcluster module | |
| print('') | |
| print('=== Testing sequenzo_fastcluster module ===') | |
| try: | |
| from sequenzo.clustering.sequenzo_fastcluster.fastcluster import linkage, linkage_vector, single, complete, average | |
| print('[OK] sequenzo_fastcluster wrapper imported successfully') | |
| # Test linkage function with small dataset | |
| from scipy.spatial.distance import pdist | |
| test_data = np.array([[1, 2], [1, 4], [1, 0], [4, 2], [4, 4], [4, 0]]) | |
| dist_matrix = pdist(test_data) | |
| # Test different linkage methods | |
| for method in ['single', 'complete', 'average']: | |
| try: | |
| Z = linkage(dist_matrix, method=method) | |
| assert Z.shape == (len(test_data) - 1, 4) | |
| assert np.all(np.isfinite(Z)) | |
| print(f'[OK] linkage({method}) works correctly') | |
| except Exception as e: | |
| print(f'[WARNING] linkage({method}) failed: {e}') | |
| # Test linkage_vector | |
| try: | |
| Z_vec = linkage_vector(test_data, method='complete', metric='euclidean') | |
| assert Z_vec.shape == (len(test_data) - 1, 4) | |
| assert np.all(np.isfinite(Z_vec)) | |
| print('[OK] linkage_vector works correctly') | |
| except Exception as e: | |
| print(f'[WARNING] linkage_vector failed: {e}') | |
| except ImportError as e: | |
| print(f'[WARNING] sequenzo_fastcluster module not available: {e}') | |
| print('[INFO] This module provides optimized hierarchical clustering') | |
| print('[INFO] All tests completed') | |
| PY | |
| # Windows test command using python -c with exec for multiline code | |
| CIBW_TEST_COMMAND_WINDOWS: | | |
| echo "Testing Sequenzo wheel on Windows" | |
| echo "" > test_sequenzo_ci.py | |
| Add-Content test_sequenzo_ci.py @" | |
| import importlib.util, sys | |
| import numpy as np | |
| import sequenzo | |
| print('Sequenzo import successful') | |
| mod = importlib.util.find_spec('sequenzo.clustering.clustering_c_code') | |
| print('Clustering C++ extensions loaded' if mod else 'WARNING: Clustering C++ extensions not found') | |
| try: | |
| from sequenzo.dissimilarity_measures import c_code | |
| print('[OK] Dissimilarity measures C++ extensions (c_code) loaded successfully') | |
| for name in ['OMdistance', 'OMspellDistance', 'DHDdistance', 'LCPdistance', 'dist2matrix']: | |
| if hasattr(c_code, name): | |
| print(f'[OK] {name} class available') | |
| except ImportError as e: | |
| print(f'ERROR: Failed to import c_code: {e}') | |
| # Check sequenzo_fastcluster module | |
| print('') | |
| print('=== Testing sequenzo_fastcluster module ===') | |
| try: | |
| from sequenzo.clustering.sequenzo_fastcluster.fastcluster import linkage, linkage_vector, single, complete, average | |
| print('[OK] sequenzo_fastcluster wrapper imported successfully') | |
| # Test linkage function with small dataset | |
| from scipy.spatial.distance import pdist | |
| test_data = np.array([[1, 2], [1, 4], [1, 0], [4, 2], [4, 4], [4, 0]]) | |
| dist_matrix = pdist(test_data) | |
| # Test different linkage methods | |
| for method in ['single', 'complete', 'average']: | |
| try: | |
| Z = linkage(dist_matrix, method=method) | |
| assert Z.shape == (len(test_data) - 1, 4) | |
| assert np.all(np.isfinite(Z)) | |
| print(f'[OK] linkage({method}) works correctly') | |
| except Exception as e: | |
| print(f'[WARNING] linkage({method}) failed: {e}') | |
| # Test linkage_vector | |
| try: | |
| Z_vec = linkage_vector(test_data, method='complete', metric='euclidean') | |
| assert Z_vec.shape == (len(test_data) - 1, 4) | |
| assert np.all(np.isfinite(Z_vec)) | |
| print('[OK] linkage_vector works correctly') | |
| except Exception as e: | |
| print(f'[WARNING] linkage_vector failed: {e}') | |
| except ImportError as e: | |
| print(f'[WARNING] sequenzo_fastcluster module not available: {e}') | |
| print('[INFO] This module provides optimized hierarchical clustering') | |
| # 为 NumPy 2.0 设置兼容性 | |
| if sys.version_info >= (3, 12): | |
| try: | |
| np._import_array() | |
| print('[OK] NumPy array imported for NumPy 2.0 compatibility') | |
| except Exception as e: | |
| print(f'[INFO] NumPy array import: {e}') | |
| print('[INFO] All tests completed') | |
| "@ | |
| python test_sequenzo_ci.py | |
| - name: Show dist content | |
| run: ls -lah dist/ | |
| shell: bash | |
| - name: Check wheels | |
| run: twine check dist/* | |
| # 验证轮子 RECORD 以适应 PyPI 最新要求 | |
| - name: Verify Wheel RECORD Integrity | |
| run: | | |
| echo "=== Verifying wheel RECORD integrity ===" | |
| for wheel in dist/*.whl; do | |
| echo "Checking $wheel" | |
| # 解压并比较 RECORD 与实际文件 | |
| unzip -q $wheel -d temp_wheel | |
| cd temp_wheel | |
| # 提取 RECORD | |
| record_file=$(find . -name RECORD) | |
| if [ -f "$record_file" ]; then | |
| # 检查每个 RECORD 条目是否存在 | |
| awk -F, '{print $1}' $record_file | while read file; do | |
| if [ ! -f "$file" ]; then | |
| echo "[ERROR] Missing file in wheel: $file" | |
| fi | |
| done | |
| # 检查多余文件(可选) | |
| find . -type f | grep -v '.dist-info/' | while read extra; do | |
| if ! grep -q "^${extra#./}," $record_file; then | |
| echo "[WARNING] Extra file not in RECORD: $extra" | |
| fi | |
| done | |
| else | |
| echo "[ERROR] No RECORD file found" | |
| fi | |
| cd .. | |
| rm -rf temp_wheel | |
| done | |
| shell: bash | |
| - name: Verify OpenMP Support | |
| run: | | |
| echo "=== Verifying built wheels ===" | |
| python -c " | |
| import subprocess | |
| import sys | |
| import os | |
| # 精确匹配当前 Python 版本的轮子 (e.g., cp39, cp310) | |
| py_tag = 'cp${{ matrix.python-version }}'.replace('.', '') | |
| wheel_files = [f for f in os.listdir('dist') if f.endswith('.whl') and py_tag in f] | |
| if wheel_files: | |
| wheel_file = wheel_files[0] | |
| print(f'Installing wheel: {wheel_file} (Python tag: {py_tag})') | |
| subprocess.run([sys.executable, '-m', 'pip', 'install', '--force-reinstall', f'dist/{wheel_file}'], check=True) | |
| import sequenzo | |
| print('Sequenzo import successful') | |
| try: | |
| import sequenzo.clustering.clustering_c_code as cc | |
| print('C++ extensions loaded successfully') | |
| except ImportError as e: | |
| print(f'WARNING: C++ extensions loading failed: {e}') | |
| else: | |
| print('ERROR: No matching wheel files found') | |
| " || echo "Verification completed with warnings" | |
| shell: bash | |
| - name: Verify built Cython modules | |
| run: | | |
| echo "=== Verifying built cython modules in wheel ===" | |
| # 安装轮子 | |
| WHEEL_FILE=$(ls dist/*.whl | head -1) | |
| echo "Installing wheel: $WHEEL_FILE" | |
| pip install --force-reinstall "$WHEEL_FILE" | |
| # 检查安装的模块 - 使用 ASCII 字符 | |
| echo "=== Checking installed modules ===" | |
| python -c " | |
| import os | |
| import sys | |
| import importlib.util | |
| # 设置标准输出编码为 UTF-8 以避免 Windows 编码问题 | |
| if sys.platform == 'win32': | |
| import io | |
| sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace') | |
| def check_module(module_name): | |
| try: | |
| spec = importlib.util.find_spec(module_name) | |
| if spec: | |
| print(f'[OK] {module_name}: {spec.origin}') | |
| return True | |
| else: | |
| print(f'[FAIL] {module_name}: NOT FOUND') | |
| return False | |
| except Exception as e: | |
| print(f'[FAIL] {module_name}: ERROR - {e}') | |
| return False | |
| # 检查主包 | |
| check_module('sequenzo') | |
| check_module('sequenzo.dissimilarity_measures') | |
| check_module('sequenzo.dissimilarity_measures.utils') | |
| # 检查 Cython 模块 | |
| cython_modules = [ | |
| 'sequenzo.dissimilarity_measures.utils.get_sm_trate_substitution_cost_matrix', | |
| 'sequenzo.dissimilarity_measures.utils.seqconc', | |
| 'sequenzo.dissimilarity_measures.utils.seqdss', | |
| 'sequenzo.dissimilarity_measures.utils.seqdur', | |
| 'sequenzo.dissimilarity_measures.utils.seqlength' | |
| ] | |
| print('\\\\n=== Checking Cython modules ===') | |
| for module in cython_modules: | |
| check_module(module) | |
| # 检查包内容 | |
| print('\\\\n=== Package contents ===') | |
| try: | |
| import sequenzo | |
| pkg_path = os.path.dirname(sequenzo.__file__) | |
| print(f'Package path: {pkg_path}') | |
| # 列出所有文件 | |
| for root, dirs, files in os.walk(pkg_path): | |
| for file in files: | |
| if file.endswith(('.so', '.pyd', '.dylib')): | |
| print(f'Shared lib: {os.path.join(root, file)}') | |
| except Exception as e: | |
| print(f'Error examining package: {e}') | |
| " | |
| shell: bash | |
| - name: Run Integration Tests (Quickstart Workflow) | |
| run: | | |
| echo "=== Running integration tests based on quickstart tutorial ===" | |
| # 详细的调试信息 | |
| echo "Current Python: $(which python)" | |
| echo "Python version: $(python --version)" | |
| echo "Platform: $(python -c 'import platform; print(platform.platform())')" | |
| echo "Architecture: $(python -c 'import platform; print(platform.machine())')" | |
| # 更智能的轮子选择逻辑 | |
| python -c " | |
| import subprocess | |
| import sys | |
| import os | |
| import platform | |
| # 获取当前平台信息 | |
| current_platform = platform.system().lower() | |
| current_machine = platform.machine().lower() | |
| python_version = f'cp{sys.version_info.major}{sys.version_info.minor}' | |
| print(f'Current platform: {current_platform}') | |
| print(f'Current machine: {current_machine}') | |
| print(f'Python version tag: {python_version}') | |
| # 查找匹配的轮子文件 | |
| wheel_files = [f for f in os.listdir('dist') if f.endswith('.whl')] | |
| print(f'Available wheel files: {wheel_files}') | |
| # 优先选择与当前平台匹配的轮子 | |
| matching_wheels = [] | |
| for wheel_file in wheel_files: | |
| # 检查平台标签 | |
| platform_ok = False | |
| arch_ok = False | |
| if current_platform == 'darwin': | |
| # macOS 平台检查 | |
| if 'macosx' in wheel_file: | |
| platform_ok = True | |
| # 检查架构 | |
| if current_machine == 'arm64' and 'arm64' in wheel_file: | |
| arch_ok = True | |
| elif current_machine == 'x86_64' and 'x86_64' in wheel_file: | |
| arch_ok = True | |
| elif current_platform == 'linux': | |
| if 'linux' in wheel_file: | |
| platform_ok = True | |
| arch_ok = True # Linux 通常兼容性更好 | |
| elif current_platform == 'windows': | |
| if 'win' in wheel_file: | |
| platform_ok = True | |
| arch_ok = True | |
| if platform_ok and arch_ok and python_version in wheel_file: | |
| matching_wheels.append(wheel_file) | |
| print(f'Matching wheels: {matching_wheels}') | |
| if matching_wheels: | |
| # 选择最匹配的轮子 | |
| wheel_file = matching_wheels[0] | |
| print(f'Selected wheel for testing: {wheel_file}') | |
| # 卸载可能存在的旧版本 | |
| subprocess.run([sys.executable, '-m', 'pip', 'uninstall', '-y', 'sequenzo'], | |
| capture_output=True) | |
| # 安装新轮子 | |
| result = subprocess.run([ | |
| sys.executable, '-m', 'pip', 'install', '--force-reinstall', | |
| f'dist/{wheel_file}' | |
| ], capture_output=True, text=True) | |
| if result.returncode == 0: | |
| print('Wheel installed successfully') | |
| print('Installation output:', result.stdout) | |
| else: | |
| print(f'Wheel installation failed: {result.stderr}') | |
| # 尝试使用 --ignore-requires-python | |
| print('Trying with --ignore-requires-python flag...') | |
| subprocess.run([ | |
| sys.executable, '-m', 'pip', 'install', '--force-reinstall', | |
| '--ignore-requires-python', f'dist/{wheel_file}' | |
| ], check=True) | |
| else: | |
| print('ERROR: No matching wheel files found') | |
| sys.exit(1) | |
| " | |
| # 检查安装状态 | |
| echo "=== Checking installation ===" | |
| python -c " | |
| import sys | |
| import subprocess | |
| # 检查已安装的包 | |
| result = subprocess.run([sys.executable, '-m', 'pip', 'list'], | |
| capture_output=True, text=True) | |
| print('Installed packages:') | |
| print(result.stdout) | |
| # 检查 sequenzo 是否在列表中 | |
| if 'sequenzo' in result.stdout: | |
| print('✓ sequenzo found in pip list') | |
| else: | |
| print('✗ sequenzo NOT found in pip list') | |
| " | |
| # 详细的导入测试 | |
| echo "=== Testing sequenzo import ===" | |
| python -c " | |
| import sys | |
| import os | |
| print('Python path:') | |
| for p in sys.path: | |
| print(f' {p}') | |
| # 尝试找到 sequenzo 包 | |
| try: | |
| import importlib.util | |
| spec = importlib.util.find_spec('sequenzo') | |
| if spec: | |
| print(f'✓ Found sequenzo at: {spec.origin}') | |
| else: | |
| print('✗ sequenzo module not found in Python path') | |
| except Exception as e: | |
| print(f'Error finding sequenzo: {e}') | |
| # 尝试导入 | |
| try: | |
| import sequenzo | |
| print('✓ Successfully imported sequenzo') | |
| print(f'Sequenzo version: {getattr(sequenzo, \"__version__\", \"unknown\")}') | |
| print(f'Sequenzo location: {sequenzo.__file__}') | |
| except ImportError as e: | |
| print(f'✗ Failed to import sequenzo: {e}') | |
| # 打印详细的错误信息 | |
| import traceback | |
| traceback.print_exc() | |
| except Exception as e: | |
| print(f'✗ Error during import: {e}') | |
| import traceback | |
| traceback.print_exc() | |
| " | |
| # 如果导入成功,运行集成测试 | |
| echo "=== Running integration tests ===" | |
| if python -c "import sequenzo" 2>/dev/null; then | |
| echo "Sequenzo imported successfully, running integration tests..." | |
| # 安装 pytest 如果尚未安装 | |
| pip install pytest || echo "pytest installation failed, but continuing..." | |
| # 检查测试文件是否存在 | |
| if [ -f "tests/test_quickstart_integration.py" ]; then | |
| pytest tests/test_quickstart_integration.py -v -s || echo "Integration tests completed with warnings" | |
| else | |
| echo "Test file not found" | |
| fi | |
| else | |
| echo "ERROR: Cannot import sequenzo, skipping integration tests" | |
| # 退出并显示错误 | |
| exit 1 | |
| fi | |
| shell: bash | |
| - name: Upload wheels | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: wheels-${{ matrix.os }}-${{ matrix.python-version }} | |
| path: dist/ |