Skip to content

CI: build wheels natively per arch (fix duplicate/colliding wheels; add win_arm64) #373

CI: build wheels natively per arch (fix duplicate/colliding wheels; add win_arm64)

CI: build wheels natively per arch (fix duplicate/colliding wheels; add win_arm64) #373

Workflow file for this run

name: Wheels
on:
workflow_dispatch:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
build_sdist:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: true
- run: pipx run build --sdist
- run: pipx run twine check dist/* || exit 1 # Ensure failure on twine check errors
- uses: actions/upload-artifact@v4
with:
name: dist-sdist
path: dist/*.tar.gz
build_wheels:
runs-on: ${{ matrix.os }}
# windows-11-arm is newly added; its native toolchain may not build every
# dependency yet (embree / predicates use x86 SIMD), so don't let that leg
# block a release if it fails.
continue-on-error: ${{ matrix.os == 'windows-11-arm' }}
strategy:
fail-fast: false
# One runner per *native* target architecture; cibuildwheel builds the
# runner's native arch by default. Do NOT reintroduce a cross `arch`
# dimension: previously `arch: arm64` on the x86 ubuntu-24.04 / windows-latest
# runners silently built x86_64 / amd64 wheels (only CIBW_ARCHS_MACOS was
# wired up), producing duplicate wheels that collided during the release
# artifact download and left win_arm64 uncovered.
matrix:
os: [ubuntu-24.04, ubuntu-24.04-arm, macos-latest, macos-15-intel, windows-latest, windows-11-arm]
pybuilds: [cp38, cp39, cp310, cp311, cp312]
exclude:
# No macOS CPython 3.8 wheels are shipped.
- os: macos-latest
pybuilds: cp38
- os: macos-15-intel
pybuilds: cp38
# Windows on ARM64 has official CPython only for 3.11+.
- os: windows-11-arm
pybuilds: cp38
- os: windows-11-arm
pybuilds: cp39
- os: windows-11-arm
pybuilds: cp310
name: >
${{ matrix.pybuilds }} ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- run: pip install cibuildwheel==3.1.4
- name: Build wheels
run: python -m cibuildwheel --output-dir wheelhouse
env:
CIBW_BEFORE_ALL_LINUX: "yum install -y clang"
CIBW_ENVIRONMENT_LINUX: "CC=clang CXX=clang++"
CIBW_MANYLINUX_X86_64_IMAGE: "manylinux_2_28"
CIBW_MANYLINUX_AARCH64_IMAGE: "manylinux_2_28"
CIBW_BUILD: "${{ matrix.pybuilds }}-*"
# No CIBW_ARCHS_* override: each runner builds its native architecture.
CIBW_SKIP: "cp*-manylinux_i686 cp*-musllinux* cp*-win32"
# embree only enables its NEON (sse2neon) path when its build detects an
# ARM target; on Windows/MSVC-ARM64 that detection falls back to SSE2 and
# fails to compile (x86 immintrin.h). Drop embree on win_arm64 (the
# igl.embree submodule is simply absent from that wheel).
CIBW_ENVIRONMENT_WINDOWS: ${{ matrix.os == 'windows-11-arm' && 'CMAKE_ARGS=-DLIBIGL_EMBREE=OFF' || '' }}
- name: Upload Artifact
uses: actions/upload-artifact@v4
continue-on-error: true # Important: Continue if upload fails
with:
# Unique per (os, python); one native arch per runner so wheel
# filenames never collide when the release job merges artifacts.
name: wheels-${{ matrix.os }}-${{ matrix.pybuilds }}
path: ./wheelhouse/*.whl
if-no-files-found: error # Fail if no wheels are found
compression-level: 6
overwrite: false
include-hidden-files: false
check_version:
name: Check for a version bump
# Only a push to main can release; PRs and workflow_dispatch never do.
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
outputs:
should_release: ${{ steps.check.outputs.should_release }}
version: ${{ steps.check.outputs.version }}
prerelease: ${{ steps.check.outputs.prerelease }}
steps:
- uses: actions/checkout@v4
with:
# Need history to read pyproject.toml as of the previous commit
fetch-depth: 0
- uses: actions/setup-python@v5
with:
python-version: '3.12' # tomllib needs >=3.11
- name: Compare pyproject.toml version against the previous commit
id: check
env:
BEFORE: ${{ github.event.before }}
run: |
set -euo pipefail
# Read project.version from pyproject.toml at a given revision
# (empty revision = the working tree)
read_version() {
if [ -z "${1:-}" ]; then cat pyproject.toml; else git show "$1:pyproject.toml"; fi \
| python3 -c 'import sys,tomllib; print(tomllib.loads(sys.stdin.read())["project"]["version"])'
}
version=$(read_version)
echo "version=$version" >> "$GITHUB_OUTPUT"
# PEP 440 pre-releases (aN/bN/rcN/.devN) are flagged as such on GitHub.
# Note `.postN` is deliberately not a pre-release.
if printf '%s' "$version" | grep -Eq '(a[0-9]+|b[0-9]+|rc[0-9]+|\.dev[0-9]*)$'; then
echo "prerelease=true" >> "$GITHUB_OUTPUT"
else
echo "prerelease=false" >> "$GITHUB_OUTPUT"
fi
# `before` is all-zeros when the branch is first created, and can point
# at a vanished commit after a force-push; fall back to the first parent.
base="$BEFORE"
if [ -z "$base" ] || [ "$base" = "0000000000000000000000000000000000000000" ] \
|| ! git cat-file -e "$base^{commit}" 2>/dev/null; then
base=$(git rev-parse --verify --quiet HEAD^ || true)
fi
if [ -z "$base" ]; then
echo "No previous commit to compare against; not releasing."
echo "should_release=false" >> "$GITHUB_OUTPUT"
exit 0
fi
previous=$(read_version "$base")
echo "previous version: $previous"
echo "current version: $version"
if [ "$version" = "$previous" ]; then
echo "Version unchanged; not releasing."
echo "should_release=false" >> "$GITHUB_OUTPUT"
exit 0
fi
# Belt and braces: PyPI refuses to overwrite an existing version, and a
# re-run of this workflow would otherwise try to. Also covers the case
# where a bump was published by some other route.
if curl -sfL -o /dev/null "https://pypi.org/pypi/libigl/$version/json"; then
echo "libigl $version is already on PyPI; not releasing."
echo "should_release=false" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "Version bumped $previous -> $version; releasing."
echo "should_release=true" >> "$GITHUB_OUTPUT"
release_artifacts:
name: Publish ${{ needs.check_version.outputs.version }} to PyPI
needs: [check_version, build_sdist, build_wheels]
if: needs.check_version.outputs.should_release == 'true'
runs-on: ubuntu-latest
permissions:
id-token: write # for PyPI trusted publishing
contents: write # to create the tag and GitHub release
steps:
- uses: actions/download-artifact@v4
with:
path: ./artifacts
merge-multiple: true
- run: ls -R ./artifacts
- name: Tag and create a GitHub release
uses: ncipollo/release-action@v1
with:
tag: ${{ needs.check_version.outputs.version }}
commit: ${{ github.sha }}
name: ${{ needs.check_version.outputs.version }}
prerelease: ${{ needs.check_version.outputs.prerelease }}
allowUpdates: true
# Auto-populate the release body with the merged PRs since the
# previous tag (curated highlights live in CHANGELOG.md).
generateReleaseNotes: true
artifacts: "./artifacts/**/*"
token: ${{ secrets.GITHUB_TOKEN }}
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@v1.13.0
with:
packages-dir: ./artifacts
skip-existing: true