Skip to content

Bump version to 0.1.38 #296

Bump version to 0.1.38

Bump version to 0.1.38 #296

Workflow file for this run

# 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 }}
CIBW_VERSION: "3.1.4"
jobs:
build:
name: Build ${{ matrix.os }} Py${{ matrix.python-version }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
max-parallel: 6
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
env:
PIP_DEFAULT_TIMEOUT: "120"
PIP_RETRIES: "10"
PIP_DISABLE_PIP_VERSION_CHECK: "1"
HOMEBREW_NO_AUTO_UPDATE: "1"
HOMEBREW_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CIBW_DEPENDENCY_VERSIONS: "pinned"
steps:
- name: Checkout source with submodules
uses: actions/checkout@v4
with:
submodules: recursive
- name: Set up Python
uses: actions/setup-python@v4
with:
# cibuildwheel>=3 requires the runner Python to be >=3.11.
# Target wheel tags are still controlled by CIBW_BUILD (cp39/cp310/cp311/cp312).
python-version: "3.11"
- name: Cache pip (Linux)
if: runner.os == 'Linux'
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-py${{ matrix.python-version }}-pip-${{ hashFiles('pyproject.toml', 'setup.py', 'setup.cfg', '.github/workflows/python-app.yml') }}
restore-keys: |
${{ runner.os }}-py${{ matrix.python-version }}-pip-
${{ runner.os }}-pip-
- name: Cache pip (macOS)
if: runner.os == 'macOS'
uses: actions/cache@v4
with:
path: ~/Library/Caches/pip
key: ${{ runner.os }}-py${{ matrix.python-version }}-pip-${{ hashFiles('pyproject.toml', 'setup.py', 'setup.cfg', '.github/workflows/python-app.yml') }}
restore-keys: |
${{ runner.os }}-py${{ matrix.python-version }}-pip-
${{ runner.os }}-pip-
- name: Cache pip (Windows)
if: runner.os == 'Windows'
uses: actions/cache@v4
with:
path: ~\AppData\Local\pip\Cache
key: ${{ runner.os }}-py${{ matrix.python-version }}-pip-${{ hashFiles('pyproject.toml', 'setup.py', 'setup.cfg', '.github/workflows/python-app.yml') }}
restore-keys: |
${{ runner.os }}-py${{ matrix.python-version }}-pip-
${{ runner.os }}-pip-
- name: Cache cibuildwheel virtualenv download (Linux)
if: runner.os == 'Linux'
uses: actions/cache@v4
with:
path: |
~/.cache/pypa/cibuildwheel/Cache
~/.cache/cibuildwheel
key: ${{ runner.os }}-py${{ matrix.python-version }}-cibw-${{ env.CIBW_VERSION }}
restore-keys: |
${{ runner.os }}-py${{ matrix.python-version }}-cibw-
${{ runner.os }}-cibw-
- name: Cache cibuildwheel virtualenv download (macOS)
if: runner.os == 'macOS'
uses: actions/cache@v4
with:
path: |
~/Library/Caches/pypa/cibuildwheel/Cache
~/Library/Caches/cibuildwheel
key: ${{ runner.os }}-py${{ matrix.python-version }}-cibw-${{ env.CIBW_VERSION }}
restore-keys: |
${{ runner.os }}-py${{ matrix.python-version }}-cibw-
${{ runner.os }}-cibw-
- name: Cache cibuildwheel virtualenv download (Windows)
if: runner.os == 'Windows'
uses: actions/cache@v4
with:
path: ~\AppData\Local\pypa\cibuildwheel\Cache
key: ${{ runner.os }}-py${{ matrix.python-version }}-cibw-${{ env.CIBW_VERSION }}
restore-keys: |
${{ runner.os }}-py${{ matrix.python-version }}-cibw-
${{ runner.os }}-cibw-
- name: Warm cibuildwheel virtualenv cache with retry
if: ${{ false }} # Disabled: this warm-up can amplify GitHub rate limits under matrix builds.
continue-on-error: true
run: |
python - <<'PY'
import os
import shutil
import sys
import time
import urllib.request
from pathlib import Path
version = "20.30.0"
url = f"https://raw.githubusercontent.com/pypa/get-virtualenv/{version}/public/virtualenv.pyz"
filename = f"virtualenv-{version}.pyz"
home = Path.home()
candidates = []
if os.name == "nt":
local_appdata = os.environ.get("LOCALAPPDATA")
if local_appdata:
candidates.append(Path(local_appdata) / "pypa" / "cibuildwheel" / "Cache" / filename)
candidates.append(home / "AppData" / "Local" / "pypa" / "cibuildwheel" / "Cache" / filename)
elif sys.platform == "darwin":
candidates.append(home / "Library" / "Caches" / "pypa" / "cibuildwheel" / "Cache" / filename)
candidates.append(home / "Library" / "Caches" / "cibuildwheel" / filename)
else:
candidates.append(home / ".cache" / "pypa" / "cibuildwheel" / "Cache" / filename)
candidates.append(home / ".cache" / "cibuildwheel" / filename)
existing = next((p for p in candidates if p.exists()), None)
if existing:
print(f"[cache-hit] {existing}")
sys.exit(0)
tmp_file = Path.cwd() / filename
for attempt in range(1, 6):
try:
print(f"[download] attempt {attempt}/5: {url}")
req = urllib.request.Request(
url,
headers={"User-Agent": "sequenzo-ci/1.0 (+github-actions)"}
)
with urllib.request.urlopen(req, timeout=120) as response:
data = response.read()
tmp_file.write_bytes(data)
print(f"[download] completed, size={len(data)} bytes")
break
except Exception as exc:
if attempt == 5:
print(f"[download] giving up after 5 attempts: {exc}")
sys.exit(0)
sleep_s = attempt * 20
print(f"[download] failed: {exc}. retry in {sleep_s}s")
time.sleep(sleep_s)
for path in candidates:
path.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(tmp_file, path)
print(f"[cache-write] {path}")
tmp_file.unlink(missing_ok=True)
PY
shell: bash
- name: Ensure setuptools/wheel for macOS
if: runner.os == 'macOS'
run: python -m pip install --upgrade --retries 10 --timeout 120 pip setuptools wheel "cibuildwheel==${CIBW_VERSION}"
# Add OpenMP support
- name: Install OpenMP (macOS)
if: runner.os == 'macOS'
run: |
install_libomp_with_retry() {
local brew_cmd="$1"
if $brew_cmd list libomp >/dev/null 2>&1; then
echo "libomp already installed"
return 0
fi
for attempt in 1 2 3; do
if $brew_cmd install libomp; then
return 0
fi
if [ "$attempt" -lt 3 ]; then
local sleep_seconds=$((attempt * 30))
echo "brew install libomp failed, retrying in ${sleep_seconds}s..."
sleep "$sleep_seconds"
fi
done
return 1
}
# 检测架构并设置正确的 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(带重试)
if /usr/local/bin/brew list libomp >/dev/null 2>&1; then
echo 'libomp already installed'
else
success=0
for attempt in 1 2 3; do
if /usr/local/bin/brew install libomp; then
success=1
break
fi
if [ \"\$attempt\" -lt 3 ]; then
sleep_seconds=\$((attempt * 30))
echo \"brew install libomp failed, retrying in \${sleep_seconds}s...\"
sleep \"\$sleep_seconds\"
fi
done
if [ \"\$success\" -ne 1 ]; then
echo 'Failed to install libomp after 3 attempts'
exit 1
fi
fi
# 获取 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"
install_libomp_with_retry brew
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"
- name: Sanitize PATH for Windows linker resolution
if: runner.os == 'Windows'
shell: pwsh
run: |
$parts = $env:PATH -split ';' | Where-Object {
$_ -and ($_ -notmatch '(?i)\\Git\\usr\\bin\\?$')
}
$cleanPath = $parts -join ';'
"PATH=$cleanPath" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
$env:PATH = $cleanPath
Write-Host "Resolved cl/link after PATH cleanup:"
where.exe cl
where.exe link
# Install dependencies with fallback strategy
- name: Install dependencies
run: |
python -m pip install --upgrade --retries 10 --timeout 120 pip setuptools wheel "cibuildwheel==${CIBW_VERSION}"
# Use oldest-supported-numpy for builds to ensure forward compatibility
pip install --retries 10 --timeout 120 oldest-supported-numpy || pip install --retries 10 --timeout 120 "numpy>=1.21.0"
pip install --retries 10 --timeout 120 --only-binary=all "scipy<1.16" || echo "scipy will be installed in cibuildwheel"
pip install --retries 10 --timeout 120 --prefer-binary Cython pybind11 build 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 --retries 10 --timeout 120 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 --retries 10 --timeout 120 "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: |
set -euo pipefail
for attempt in 1 2 3; do
echo "cibuildwheel attempt ${attempt}/3"
if python -m cibuildwheel --output-dir dist; then
exit 0
fi
if [ "$attempt" -lt 3 ]; then
sleep_seconds=$((attempt * 30))
echo "cibuildwheel failed, retrying in ${sleep_seconds}s..."
sleep "$sleep_seconds"
fi
done
echo "cibuildwheel failed after 3 attempts"
exit 1
shell: bash
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
# 显式将 runner 级别的环境变量传入 cibuildwheel 的隔离 venv
# 避免 $LIBOMP_PATH 因隐式继承失败而导致 delocate 找不到 libomp
CIBW_PASS_ENV: "LIBOMP_PATH SEQUENZO_ENABLE_OPENMP"
# macOS: Set OpenMP environment with proper library paths and rpath for bundling
# REPAIR_LIBRARY_PATH 是 libomp 路径的副本,专用于在 repair 阶段
# 内部设置 DYLD_LIBRARY_PATH(macOS SIP 会剥离子进程继承的 DYLD_LIBRARY_PATH)
CIBW_ENVIRONMENT_MACOS: >
SEQUENZO_ENABLE_OPENMP=1
REPAIR_LIBRARY_PATH="$LIBOMP_PATH"
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
# 修复关键点:
# 1. 在 repair 命令内部显式设置 DYLD_LIBRARY_PATH
# macOS SIP 会在子进程启动时剥离 DYLD_LIBRARY_PATH,
# 所以必须在 shell 命令内临时设置,delocate 才能找到 libomp.dylib。
# 2. 添加 --require-archs {delocate_archs} 确保打包的 dylib 架构
# 与 .so 文件一致(防止 arm64/x86_64 混淆导致用户端 crash)。
CIBW_REPAIR_WHEEL_COMMAND_MACOS: |
echo "=== Repairing macOS wheel ==="
echo "Current architecture: $(uname -m)"
echo "Wheel: {wheel}"
echo "REPAIR_LIBRARY_PATH: ${REPAIR_LIBRARY_PATH}"
# 在 repair 命令内部设置 DYLD_LIBRARY_PATH,绕过 macOS SIP 的剥离
DYLD_LIBRARY_PATH="${REPAIR_LIBRARY_PATH}:${DYLD_LIBRARY_PATH:-}" \
delocate-wheel --require-archs {delocate_archs} -w {dest_dir} -v {wheel} || {
echo "[WARNING] delocate with --require-archs failed, retrying without arch check..."
DYLD_LIBRARY_PATH="${REPAIR_LIBRARY_PATH}:${DYLD_LIBRARY_PATH:-}" \
delocate-wheel -w {dest_dir} -v {wheel} || {
echo "[WARNING] delocate failed completely, copying wheel as-is"
mkdir -p {dest_dir}
cp {wheel} {dest_dir}/
}
}
# 验证修复结果:确认 libomp 是否真的打包进去了
if ls {dest_dir}/*.whl 1> /dev/null 2>&1; then
REPAIRED_WHEEL=$(ls {dest_dir}/*.whl | head -1)
echo "Repaired wheel: $REPAIRED_WHEEL"
echo "=== Bundled dependencies ==="
delocate-listdeps --all "$REPAIRED_WHEEL" 2>&1 || echo "Dependency listing failed"
if delocate-listdeps --all "$REPAIRED_WHEEL" 2>&1 | grep -q "libomp"; then
echo "[OK] libomp successfully bundled into wheel"
else
echo "[WARNING] libomp may not be bundled - check wheel manually"
fi
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"
for /f "usebackq tokens=*" %%i in (`"%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe" -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -find VC\Tools\MSVC\**\bin\Hostx64\x64`) do set "VCTOOLS_BIN=%%i"
if not defined VCTOOLS_BIN (
echo "ERROR: Unable to locate MSVC linker path via vswhere"
exit /b 1
)
set "PATH=%VCTOOLS_BIN%;%PATH%"
where cl
where link
echo "Using minimal build dependencies"
python -m pip install --upgrade pip
# 注意:CIBW_BEFORE_BUILD(通用)在所有平台都提供了平台专用版本时
# 永远不会执行(平台专用变体会覆盖通用变体),已移除死代码。
# 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: Set up Python for wheel verification
uses: actions/setup-python@v4
with:
# cibuildwheel is driven by Python 3.11, but wheel install/import
# checks must run on the target interpreter for this matrix entry.
python-version: ${{ matrix.python-version }}
- 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 ==="
# 安装与当前 matrix Python 版本匹配的轮子
py_tag="cp${{ matrix.python-version-nodot }}"
WHEEL_FILE=$(ls dist/*.whl | grep "$py_tag" | head -1 || true)
if [ -z "$WHEEL_FILE" ]; then
echo "No wheel matches tag $py_tag in dist/"
ls -la dist/
exit 1
fi
echo "Installing wheel: $WHEEL_FILE"
python -m 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/