Skip to content

Add native macOS wheels for Intel and Apple Silicon #25

Add native macOS wheels for Intel and Apple Silicon

Add native macOS wheels for Intel and Apple Silicon #25

Workflow file for this run

name: Build wheels
# Publishing is deliberately never automatic. A PyPI filename can only be
# uploaded once -- deleting or yanking a release does NOT free the version
# number -- so a botched 0.4.0 would burn 0.4.0 permanently. Every trigger here
# only produces downloadable artifacts; uploading is a separate manual run with
# an explicit target, and the recommended first target is TestPyPI.
on:
# Branch pushes and pull requests both give a build without touching master.
# Note workflow_dispatch below only appears in the Actions UI once this file
# is on the default branch -- that is a GitHub restriction, not a choice
# here -- so until then, pushing to one of these branches is how you run it.
push:
branches:
- master
- "feature/**"
pull_request:
workflow_dispatch:
inputs:
publish:
description: "Upload the built distributions where?"
type: choice
default: none
options: [none, testpypi, pypi]
permissions:
contents: read
jobs:
plan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
with:
python-version: "3.12"
- name: Check package version and publication target
env:
RELEASE_TARGET: ${{ github.event_name == 'workflow_dispatch' && inputs.publish || 'none' }}
run: python tools/check_release.py --target "$RELEASE_TARGET"
sdist:
needs: plan
name: sdist
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
with:
python-version: "3.12"
- run: pipx run build --sdist
# The sdist shipped no C++ at all from the initial release commit until
# MANIFEST.in was added: CMakeExtension declares sources=[], so setuptools
# cannot infer the build tree. It is invisible in a wheel-only world and
# only bites whoever falls back to building from source, so assert it here
# rather than find out from a bug report.
- name: Verify the sdist can actually build
run: |
set -euo pipefail
tar tzf dist/*.tar.gz > files.txt
for pattern in \
'CMakeLists\.txt$' \
'lib/pybind11/include/pybind11/pybind11\.h$' \
'lib/ace/ace-evaluator/CMakeLists\.txt$' \
'src/pyace/maxvol/_maxvol\.pyx$'
do
grep -qE "$pattern" files.txt || { echo "sdist is missing: $pattern"; exit 1; }
done
cpp=$(grep -cE '\.cpp$' files.txt)
echo "sdist carries $cpp .cpp files"
[ "$cpp" -ge 60 ] || { echo "expected >=60 .cpp files, got $cpp"; exit 1; }
- uses: actions/upload-artifact@v7
with:
name: release-sdist
path: dist/*.tar.gz
wheels:
needs: plan
name: wheels (${{ matrix.platform }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- platform: linux-x86_64
os: ubuntu-latest
arch: x86_64
- platform: macos-arm64
os: macos-15
arch: arm64
- platform: macos-x86_64
os: macos-15-intel
arch: x86_64
steps:
- uses: actions/checkout@v7
- name: Build wheels
uses: pypa/cibuildwheel@v4.2.0
env:
CIBW_ARCHS_LINUX: ${{ matrix.arch }}
CIBW_ARCHS_MACOS: ${{ matrix.arch }}
MACOSX_DEPLOYMENT_TARGET: "11.0"
# Routine commits build one interpreter; all five are built only when
# asked for, so a normal push costs one build rather than five. Ask
# either with a manual run, or -- since workflow_dispatch is invisible
# until this file reaches the default branch -- by putting
# [full-matrix] in the pushed commit's message.
#
# Upper bound is pybind11 3.1, which supports 3.9 through 3.14; the
# floor matches requires-python. head_commit is absent on
# pull_request events, where contains() is simply false.
CIBW_BUILD: >-
${{ (github.event_name == 'workflow_dispatch'
|| contains(github.event.head_commit.message, '[full-matrix]'))
&& 'cp39-* cp310-* cp311-* cp312-* cp313-*'
|| 'cp312-*' }}
# glibc 2.28 -- RHEL/Alma/Rocky 8+, Ubuntu 20.04+, Debian 10+. This is
# the floor the dependency set allows, not a compromise.
#
# An earlier revision tried manylinux2014 (glibc 2.17) on the theory
# that pip would resolve back to an older numpy/scipy that publishes
# 2_17 wheels. It does not: pip prefers the newest *version* and falls
# back to its sdist, so it downloaded numpy 2.5.2.tar.gz and died on
# "NumPy requires GCC >= 10.3". Pinning around that would not help
# either, because cibuildwheel installs the runtime dependencies in
# the same container to run the tests, and current pandas and
# scikit-learn publish x86_64 wheels only for 2_28 -- as would any
# actual glibc-2.17 user, who would get our wheel followed by a source
# build of pandas. There is no reach to be had below 2_28 while
# numpy, scipy, pandas and scikit-learn all stop there.
CIBW_MANYLINUX_X86_64_IMAGE: manylinux_2_28
# PYACE_PORTABLE_MATH is deliberately NOT set: 2_28 whitelists
# libmvec and permits GLIBC_2.22, so -Ofast costs nothing here. The
# option stays in CMakeLists.txt for whenever a lower floor is worth
# chasing again.
# Each of these has actually been broken in this project at some point,
# so the wheel is not accepted until all four hold.
# 1. importlib.metadata looks up the DISTRIBUTION name (python-ace),
# not the import package (pyace) -- "unknown" means that broke.
# 2. all seven pybind11 modules load (pybind11 2.11 -> 3.1 upgrade).
# 3. the compiled maxvol kernel is present, not the slow scipy
# fallback -- it silently vanished from wheels before.
# 4. a console entry point resolves, since bin/ scripts became
# [project.scripts].
CIBW_TEST_REQUIRES: pytest
CIBW_TEST_COMMAND: >-
python -c "import pyace; print('version:', pyace.__version__); assert pyace.__version__ != 'unknown', 'distribution-name lookup failed'"
&& python -c "import pyace.sharmonics, pyace.coupling, pyace.basis, pyace.evaluator, pyace.catomicenvironment, pyace.calculator, pyace.grace_fs; print('7 pybind11 modules ok')"
&& python -c "from pyace.maxvol import c_maxvol; import numpy as np; i, c = c_maxvol(np.random.rand(64, 8), 1.05, 100, -1, 0); print('compiled maxvol ok, selected', len(i), 'rows')"
&& pace_info --help > /dev/null && echo 'entry points ok'
&& cd "{project}" && python -m pytest tests/test_PyACECalculator.py -q
# MANIFEST.in names the C++ sources so the sdist can build, and they sit
# inside the package tree -- so a lapse in include-package-data silently
# installs all of them into every wheel. It happened once already.
- name: Verify the wheels ship no source
run: |
set -euo pipefail
for whl in wheelhouse/*.whl; do
bad=$(unzip -Z1 "$whl" | grep -E '\.(cpp|h|pyx|c)$' || true)
if [ -n "$bad" ]; then
echo "$(basename "$whl") ships source files:"; echo "$bad"; exit 1
fi
n=$(unzip -Z1 "$whl" | grep -cE '\.so$')
echo "$(basename "$whl"): $n compiled modules, no source"
[ "$n" -eq 8 ] || { echo "expected 8 .so (7 pybind11 + _maxvol), got $n"; exit 1; }
done
- uses: actions/upload-artifact@v7
with:
name: release-wheels-${{ matrix.platform }}
path: wheelhouse/*.whl
validate:
needs: [sdist, wheels]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
with:
python-version: "3.12"
- uses: actions/download-artifact@v8
with:
pattern: release-*
path: dist
merge-multiple: true
- run: python -m pip install twine
- name: Validate distribution metadata and wheel coverage
env:
FULL_MATRIX: ${{ github.event_name == 'workflow_dispatch' || contains(github.event.head_commit.message, '[full-matrix]') }}
run: |
python -m twine check --strict dist/*
if [ "$FULL_MATRIX" = "true" ]; then
python tools/check_release.py --artifacts dist --full-matrix
else
python tools/check_release.py --artifacts dist
fi
publish-testpypi:
name: Publish to TestPyPI
needs: validate
if: github.event_name == 'workflow_dispatch' && inputs.publish == 'testpypi'
runs-on: ubuntu-latest
environment:
name: testpypi
url: https://test.pypi.org/project/python-ace/
concurrency:
group: publish-python-ace-testpypi
cancel-in-progress: false
permissions:
id-token: write
steps:
- uses: actions/download-artifact@v8
with:
pattern: release-*
path: dist
merge-multiple: true
- uses: pypa/gh-action-pypi-publish@release/v1
with:
password: ${{ secrets.TEST_PYPI_API_TOKEN }}
attestations: ${{ secrets.TEST_PYPI_API_TOKEN == '' }}
repository-url: https://test.pypi.org/legacy/
publish-pypi:
name: Publish to PyPI
needs: validate
if: github.event_name == 'workflow_dispatch' && inputs.publish == 'pypi'
runs-on: ubuntu-latest
environment:
name: pypi
url: https://pypi.org/project/python-ace/
concurrency:
group: publish-python-ace-pypi
cancel-in-progress: false
permissions:
id-token: write
steps:
- uses: actions/download-artifact@v8
with:
pattern: release-*
path: dist
merge-multiple: true
- uses: pypa/gh-action-pypi-publish@release/v1
with:
user: ${{ secrets.PYPI_USERNAME || '__token__' }}
password: ${{ secrets.PYPI_PASSWORD }}
attestations: ${{ secrets.PYPI_PASSWORD == '' }}