Repository navigation
CI: build wheels natively per arch (fix duplicate/colliding wheels; add win_arm64) #373
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
| 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 |