Skip to content

Bump version to 0.1.40 #319

Bump version to 0.1.40

Bump version to 0.1.40 #319

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'
- os: ubuntu-latest
python-version: '3.13'
python-version-nodot: '313'
- os: ubuntu-latest
python-version: '3.14'
python-version-nodot: '314'
# 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'
- os: windows-latest
python-version: '3.13'
python-version-nodot: '313'
# - os: windows-latest
# python-version: '3.14'
# python-version-nodot: '314'
# 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
- os: macos-15-intel
python-version: '3.13'
python-version-nodot: '313'
macos-arch: 'x86_64'
macos-group: intel
- os: macos-15-intel
python-version: '3.14'
python-version-nodot: '314'
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
- os: macos-latest
python-version: '3.13'
python-version-nodot: '313'
macos-arch: 'arm64'
macos-group: apple-silicon
- os: macos-latest
python-version: '3.14'
python-version-nodot: '314'
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}"
# Build libomp from LLVM source so the bundled dylib's minos matches the
# wheel deployment target (Homebrew libomp 18+ requires macOS 13+, which
# would break the minos guarantee of macosx_10_15 / macosx_11_0 wheels).
#
# Previously this caused "symbol not found in flat namespace
# '___kmpc_dispatch_deinit'" at import time. Root cause: that symbol is
# an Intel-private OpenMP extension emitted by clang's -fopenmp when
# dynamic/guided schedules are used, but the LLVM openmp *runtime* we build
# from source deliberately omits it (it was removed in LLVM 13).
# Fix: pass -fno-openmp-extensions at compile time so clang never emits a
# reference to __kmpc_dispatch_deinit (or any other Intel-private symbol).
# The resulting .so is pure OpenMP-standard and links cleanly against our
# self-built libomp whose minos == the wheel deployment target.
- name: Cache CI libomp (macOS)
if: runner.os == 'macOS'
uses: actions/cache@v4
with:
path: .local-libomp
key: macos-libomp-${{ matrix.macos-arch }}-${{ matrix.macos-arch == 'x86_64' && '10.15' || '11.0' }}-llvm18.1.8-${{ hashFiles('maintenance_scripts/build_macos_ci_libomp.sh') }}
- name: Install OpenMP (macOS)
if: runner.os == 'macOS'
run: |
set -euo pipefail
if [ "${{ matrix.macos-arch }}" == "x86_64" ]; then
MACOS_DEPLOY_TARGET="10.15"
else
MACOS_DEPLOY_TARGET="11.0"
fi
chmod +x maintenance_scripts/build_macos_ci_libomp.sh
maintenance_scripts/build_macos_ci_libomp.sh \
"${{ matrix.macos-arch }}" \
"$MACOS_DEPLOY_TARGET" \
"$GITHUB_WORKSPACE/.local-libomp"
LIBOMP_PREFIX="$GITHUB_WORKSPACE/.local-libomp"
echo "CI libomp prefix: $LIBOMP_PREFIX"
file "$LIBOMP_PREFIX/lib/libomp.dylib"
otool -l "$LIBOMP_PREFIX/lib/libomp.dylib" | awk '/LC_BUILD_VERSION|LC_VERSION_MIN_MACOSX|minos|version/'
{
echo "CC=clang"
echo "CXX=clang++"
echo "MACOSX_DEPLOYMENT_TARGET=$MACOS_DEPLOY_TARGET"
echo "LDFLAGS=-L$LIBOMP_PREFIX/lib -Wl,-rpath,@loader_path/../.dylibs"
echo "CPPFLAGS=-I$LIBOMP_PREFIX/include"
echo "SEQUENZO_ENABLE_OPENMP=1"
echo "SEQUENZO_LIBOMP_PREFIX=$LIBOMP_PREFIX"
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
- name: Locate MSVC LLVM OpenMP runtime (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: |
$vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe"
$installPath = & $vswhere -latest -products * -property installationPath
if (-not $installPath) {
throw "vswhere did not return a Visual Studio installation path"
}
$releaseMatches = Get-ChildItem -Path "$installPath\VC\Redist\MSVC\*\x64\Microsoft.VC*.OpenMP.LLVM\libomp140.x86_64.dll" -ErrorAction SilentlyContinue | Sort-Object FullName
$debugMatches = Get-ChildItem -Path "$installPath\VC\Redist\MSVC\*\debug_nonredist\x64\Microsoft.VC*.OpenMP.LLVM\libomp140.x86_64.dll" -ErrorAction SilentlyContinue | Sort-Object FullName
$llvmMatches = @(
Get-ChildItem -Path "$installPath\VC\Tools\Llvm\x64\bin\libomp140.x86_64.dll" -ErrorAction SilentlyContinue
Get-ChildItem -Path "$installPath\VC\Tools\Llvm\x64\bin\libomp140.dll" -ErrorAction SilentlyContinue
) | Sort-Object FullName
$dll = $null
if ($releaseMatches) {
$dll = $releaseMatches[-1]
} elseif ($debugMatches) {
$dll = $debugMatches[-1]
Write-Host "WARNING: using debug_nonredist OpenMP runtime: $($dll.FullName)"
} elseif ($llvmMatches) {
$dll = $llvmMatches[-1]
}
if (-not $dll) {
throw "libomp140*.dll not found under Visual Studio install: $installPath"
}
Write-Host "Found OpenMP runtime: $($dll.FullName)"
"LIBOMP_DLL_DIR=$($dll.DirectoryName)" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
# 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: 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
# hmmlearn has no Windows py3.14 binary wheel yet (3.14 is still pre-release)
# and its source build fails due to link.exe PATH issues in cibuildwheel's
# isolated test venv. Skip the test phase only; the wheel is still built.
CIBW_TEST_SKIP: "cp314-win_amd64"
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
# 或者根据架构设置不同的目标
# Build libomp from source in CI so bundled dylib matches these targets.
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 LIBOMP_DLL_DIR SEQUENZO_LIBOMP_PREFIX SEQUENZO_ENABLE_OPENMP MACOSX_DEPLOYMENT_TARGET GITHUB_WORKSPACE"
# 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"
CFLAGS="-fno-openmp-extensions"
CXXFLAGS="-fno-openmp-extensions"
# 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
# 验证 CI 构建的 libomp(兼容 wheel 最低 macOS 版本)
if [ -n "${SEQUENZO_LIBOMP_PREFIX:-}" ] && [ -f "$SEQUENZO_LIBOMP_PREFIX/lib/libomp.dylib" ]; then
echo "[OK] CI libomp found at: $SEQUENZO_LIBOMP_PREFIX"
file "$SEQUENZO_LIBOMP_PREFIX/lib/libomp.dylib"
elif [ -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)。
# 3. wheel repair 失败时必须让 CI 失败,不能发布未打包 libomp 的 macOS wheel。
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 "[ERROR] delocate failed; refusing to publish an unrepaired macOS wheel."
exit 1
}
# 验证修复结果:确认 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 "[ERROR] libomp is not bundled into the macOS wheel."
exit 1
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 ./sequenzo -name "*.so" -type f -delete 2>/dev/null || true
find . -maxdepth 1 -name "*.so" -type f -delete 2>/dev/null || true
find ./sequenzo -path "*/utils/*.c" -type f -print0 | while IFS= read -r -d '' cfile; do
pyxfile="${cfile%.c}.pyx"
if [ -f "$pyxfile" ]; then
rm -f "$cfile"
fi
done
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}"
# wheel repair 失败时必须让 CI 失败,不能发布未修复 wheel。
auditwheel repair -w {dest_dir} {wheel} || {
echo "[ERROR] auditwheel repair failed; refusing to publish an unrepaired Linux wheel."
exit 1
}
# 检查修复结果
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 "Installing wheel repair dependencies"
python -m pip install --upgrade pip
python -m pip install delvewheel
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"
CIBW_REPAIR_WHEEL_COMMAND_WINDOWS: python maintenance_scripts/repair_windows_wheel.py "{dest_dir}" "{wheel}"
# 注意: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 smoke test uses cmd-compatible python -c.
CIBW_TEST_COMMAND_WINDOWS: |
echo "Testing Sequenzo wheel on Windows"
python -c "import numpy as np; from sequenzo import SequenceData, get_distance_matrix, Cluster; import sequenzo.utils.core_distance_operations.core_distance_c_code as core_distance_c_code; from sequenzo.utils.core_distance_operations import weighted_inertia_contrib; contrib = weighted_inertia_contrib(np.array([[0.0, 1.0], [1.0, 0.0]], dtype=np.float64), np.array([0, 1], dtype=np.int32), np.array([1.0, 1.0], dtype=np.float64)); assert contrib.shape == (2,); print('[OK] Windows wheel smoke test passed')"
- 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 }}
# allow-prereleases ensures 3.14 (beta) can be installed when it hasn't been officially released yet
allow-prereleases: true
- 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"
# On Windows, git-bash may re-introduce C:/Program Files/Git/usr/bin into
# PATH, causing Git's link.exe to shadow MSVC's when pip builds packages
# from source (e.g. hmmlearn on Python 3.14 where no binary wheel exists).
if [[ "$RUNNER_OS" == "Windows" ]]; then
PATH=$(echo ":$PATH:" | tr ':' '\n' | grep -iv '/git/usr/bin' | grep -iv '\\Git\\usr\\bin' | grep . | tr '\n' ':' | sed 's/^://;s/:$//')
export PATH
fi
# Try a full install first; if it fails (e.g. hmmlearn source build on
# pre-release Python where no binary wheel exists), fall back to --no-deps
# and install hmmlearn separately with --prefer-binary.
if ! python -m pip install --force-reinstall "$WHEEL_FILE"; then
echo "Full install failed; retrying with --no-deps + prefer-binary hmmlearn..."
python -m pip install --force-reinstall --no-deps "$WHEEL_FILE"
python -m pip install "hmmlearn>=0.2.0" --prefer-binary --retries 5 --timeout 120 \
|| echo "INFO: hmmlearn binary wheel not yet available for this Python version (pre-release); skipping"
fi
# 检查安装的模块 - 使用 ASCII 字符
echo "=== Checking installed modules ==="
python -c "
import os
import sys
# Remove '' (cwd) from sys.path so Python finds the installed wheel in
# site-packages rather than the source tree, which may contain stale
# cpython-311 .so files built by the pre-cibuildwheel 'Build Cython
# extensions' step and would cause ABI-mismatch segfaults on py3.12+.
sys.path = [p for p in sys.path if p]
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: Set up Miniconda for Windows wheel smoke
if: runner.os == 'Windows'
uses: conda-incubator/setup-miniconda@v3
with:
python-version: ${{ matrix.python-version }}
activate-environment: sequenzo-wheel-smoke
auto-update-conda: true
- name: Windows Conda wheel smoke test
if: runner.os == 'Windows'
shell: bash -el {0}
run: |
echo "=== Windows Conda wheel smoke test ==="
conda info
python -m pip install --upgrade pip
echo "Installing $WHEEL into Conda environment"
python - <<'PY'
import glob
import os
import zipfile
py_tag = "cp${{ matrix.python-version-nodot }}"
wheels = sorted(glob.glob(f"dist/*{py_tag}*win_amd64.whl"))
if not wheels:
raise SystemExit(f"No Windows wheel found for {py_tag}")
wheel = os.path.abspath(wheels[-1])
with zipfile.ZipFile(wheel) as zf:
native = [
name
for name in zf.namelist()
if "core_distance_c_code" in name.replace("\\", "/")
and name.lower().endswith((".pyd", ".so"))
]
if not native:
raise SystemExit(
f"Wheel missing core_distance_c_code binary: {wheel}"
)
print(f"[OK] wheel contains core_distance_c_code: {native[0]}")
with open(os.path.join(os.environ["RUNNER_TEMP"], "sequenzo-wheel-smoke.env"), "w", encoding="utf-8") as fh:
fh.write(f"WHEEL={wheel}\n")
PY
python -m pip install --force-reinstall "$(grep '^WHEEL=' "$RUNNER_TEMP/sequenzo-wheel-smoke.env" | cut -d= -f2-)"
mkdir -p "$RUNNER_TEMP/sequenzo-wheel-smoke"
cd "$RUNNER_TEMP/sequenzo-wheel-smoke"
python - <<'PY'
import importlib.util
import os
import sys
workspace = os.path.abspath(os.environ.get("GITHUB_WORKSPACE", ""))
sys.path = [
p
for p in sys.path
if p
and (not workspace or os.path.abspath(p) != workspace)
]
import numpy as np
print("CONDA_PREFIX=" + os.environ.get("CONDA_PREFIX", ""))
print("CWD=" + os.getcwd())
from sequenzo import SequenceData, get_distance_matrix, Cluster
import sequenzo.utils.core_distance_operations.core_distance_c_code as core_distance_c_code
from sequenzo.utils.core_distance_operations import weighted_inertia_contrib
spec = importlib.util.find_spec(
"sequenzo.utils.core_distance_operations.core_distance_c_code"
)
if spec is None or not spec.origin:
raise SystemExit(
"core_distance_c_code not found after wheel install; "
f"sys.path={sys.path!r}"
)
print(f"[OK] core_distance_c_code loaded from: {spec.origin}")
contrib = weighted_inertia_contrib(
np.array([[0.0, 1.0], [1.0, 0.0]], dtype=np.float64),
np.array([0, 1], dtype=np.int32),
np.array([1.0, 1.0], dtype=np.float64),
)
assert contrib.shape == (2,)
print("[OK] Windows Conda wheel smoke test passed")
PY
- name: Upload wheels
uses: actions/upload-artifact@v4
with:
name: wheels-${{ matrix.os }}-${{ matrix.python-version }}
path: dist/