Repository navigation
Add native macOS wheels for Intel and Apple Silicon #25
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: 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 == '' }} |