From c34ef81761b48640cf12a360f888954213ed842d Mon Sep 17 00:00:00 2001 From: Yury Lysogorskiy Date: Sat, 31 Oct 2026 18:44:35 +0100 Subject: [PATCH 1/3] Build and validate native macOS wheels for Intel and Apple Silicon --- .github/RELEASING.md | 12 ++++++++---- .github/workflows/wheels.yml | 29 ++++++++++++++++++++++------- tools/check_release.py | 24 +++++++++++++++++++----- 3 files changed, 49 insertions(+), 16 deletions(-) diff --git a/.github/RELEASING.md b/.github/RELEASING.md index 9122527..687c289 100644 --- a/.github/RELEASING.md +++ b/.github/RELEASING.md @@ -1,17 +1,21 @@ # Building and publishing python-ace The **Build wheels** GitHub Actions workflow (`.github/workflows/wheels.yml`) -builds a source distribution and Linux x86-64 manylinux_2_28 wheels. Pull +builds a source distribution, Linux x86-64 manylinux_2_28 wheels, and native +macOS wheels for Apple Silicon (arm64) and Intel (x86-64). Pull requests and normal pushes build Python 3.12; manual runs build Python 3.9, 3.10, 3.11, 3.12, and 3.13. The wheels are installed and smoke-tested, then all distributions pass metadata, package-content, and version checks. -This workflow currently produces Linux x86-64 wheels; other platforms can -build from the source distribution with a C++ compiler. +Each macOS architecture is built and smoke-tested on its matching native +runner. A manual release requires 15 wheels: five Python versions on each +of the three platforms. Other platforms can build from the source distribution +with a C++ compiler. ## Build without uploading In GitHub Actions, select **Build wheels → Run workflow** and leave `publish` -as `none`. Download `release-sdist` and `release-wheels-linux-x86_64` from the +as `none`. Download `release-sdist` and `release-wheels-linux-x86_64`, +`release-wheels-macos-arm64`, and `release-wheels-macos-x86_64` from the completed run. This works before allocating a new release version. ```bash diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index ec42619..d656dbc 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -80,15 +80,30 @@ jobs: wheels: needs: plan - name: wheels (linux x86_64) - runs-on: ubuntu-latest + 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: x86_64 + 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 @@ -101,8 +116,8 @@ jobs: CIBW_BUILD: >- ${{ (github.event_name == 'workflow_dispatch' || contains(github.event.head_commit.message, '[full-matrix]')) - && 'cp39-manylinux_x86_64 cp310-manylinux_x86_64 cp311-manylinux_x86_64 cp312-manylinux_x86_64 cp313-manylinux_x86_64' - || 'cp312-manylinux_x86_64' }} + && '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. @@ -158,7 +173,7 @@ jobs: - uses: actions/upload-artifact@v7 with: - name: release-wheels-linux-x86_64 + name: release-wheels-${{ matrix.platform }} path: wheelhouse/*.whl validate: @@ -177,7 +192,7 @@ jobs: - run: python -m pip install twine - name: Validate distribution metadata and wheel coverage env: - FULL_MATRIX: ${{ github.event_name == 'workflow_dispatch' }} + 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 diff --git a/tools/check_release.py b/tools/check_release.py index 5554dc0..c389ee4 100644 --- a/tools/check_release.py +++ b/tools/check_release.py @@ -58,11 +58,25 @@ def check_metadata(raw, filename): if len(entries) != 1: raise SystemExit("Expected one top-level PKG-INFO in the source distribution.") check_metadata(archive.extractfile(entries[0]).read(), sdists[0]) - if args.full_matrix: - expected = {"cp39", "cp310", "cp311", "cp312", "cp313"} - actual = [w.name.split("-")[2] for w in wheels] - if set(actual) != expected or len(actual) != len(expected): - raise SystemExit(f"Expected exactly one wheel per interpreter {sorted(expected)}, found {actual}") + interpreters = {"cp39", "cp310", "cp311", "cp312", "cp313"} if args.full_matrix else {"cp312"} + platforms = {"linux-x86_64", "macos-arm64", "macos-x86_64"} + actual = [] + for wheel in wheels: + interpreter = wheel.name.split("-")[2] + tag = wheel.name.rsplit("-", 1)[1].removesuffix(".whl") + if tag.startswith("manylinux") and tag.endswith("_x86_64"): + platform = "linux-x86_64" + elif tag.startswith("macosx_") and tag.endswith("_arm64"): + platform = "macos-arm64" + elif tag.startswith("macosx_") and tag.endswith("_x86_64"): + platform = "macos-x86_64" + else: + raise SystemExit(f"Unexpected wheel platform: {wheel.name}") + actual.append((interpreter, platform)) + expected = {(interpreter, platform) for interpreter in interpreters for platform in platforms} + if set(actual) != expected or len(actual) != len(expected): + missing = sorted(expected - set(actual)) + raise SystemExit(f"Wheel coverage incomplete or duplicated; missing={missing}, found={actual}") print(f"Validated {len(wheels)} wheels and one source distribution for {name} {version}.") From bbab6f9a31705b5e0dcd6b185a4bb6fde3b40151 Mon Sep 17 00:00:00 2001 From: Yury Lysogorskiy Date: Sat, 31 Oct 2026 18:46:26 +0100 Subject: [PATCH 2/3] Check installed-wheel calculator behavior on every platform --- .github/workflows/wheels.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index d656dbc..5a02364 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -149,11 +149,13 @@ jobs: # 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 From 240796486b1920437d77dfd2b3905ed8eea9541c Mon Sep 17 00:00:00 2001 From: Yury Lysogorskiy Date: Sat, 31 Oct 2026 18:52:49 +0100 Subject: [PATCH 3/3] Keep Linux release wheels on the intended manylinux target --- .github/workflows/wheels.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 5a02364..25baaff 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -101,6 +101,7 @@ jobs: - name: Build wheels uses: pypa/cibuildwheel@v4.2.0 env: + CIBW_SKIP: "*-musllinux_*" CIBW_ARCHS_LINUX: ${{ matrix.arch }} CIBW_ARCHS_MACOS: ${{ matrix.arch }} MACOSX_DEPLOYMENT_TARGET: "11.0"