From 6cbcc166480b21ec0bde09634d6def70cb7cc5e9 Mon Sep 17 00:00:00 2001 From: Ben Mather Date: Thu, 23 Jul 2026 11:44:02 +1000 Subject: [PATCH 1/9] Build against NumPy 2.x and harden the meson/CI build (refs #114) PyPI wheels were compiled against NumPy 1.x and crash at import under NumPy 2.x. Build against NumPy 2.x (binaries then run on both 1.x and 2.x) and fix the surrounding release/build machinery. - Replace NumPy aliases removed in 1.24 (np.int/np.bool) - pyproject: build-requires numpy>=2.0, runtime numpy>=1.19.3; drop dead setuptools config; add py3.13 - Remove obsolete numpy.distutils setup.py/setup.cfg build path - meson.build: drop the os.chdir numpy-include hack, fix malformed rpath link args, exclude cruft from install, accurate f2py comments - pypi.yml: fix actions/upload-artifact@v4 duplicate-name failure (unique per-job names + download pattern/merge-multiple); bump actions; cibuildwheel 2.21.3; add scipy to wheel test deps - CI.yml: matrix over py3.9-3.13 x numpy{<2,>=2}; run pytest from a neutral dir so the installed package (not the source shadow) is tested - Modernise doc workflows and the Fortran-setup composite action - Remove the stale in-repo conda recipe (superseded by the conda-forge feedstock), dead CI env files and stray build artifacts; widen .gitignore; refresh README badges/channel; add a changelog entry Verified locally: builds against numpy 2.5.1 and all 46 tests pass under numpy 2.5.1, including test_linear_interpolation. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01FFBEqxMhk9Ut6vVm5jfV8d --- .github/workflows/CI.yml | 54 +++++--- .github/workflows/build_deploy_jbdoc.yml | 35 +++-- .github/workflows/build_deploy_pdoc.yml | 32 ++--- .github/workflows/pypi.yml | 27 ++-- .../resources/conda_api_docs_environment.yml | 11 -- .../resources/conda_build_environment.yml | 13 -- .../resources/conda_jb_docs_environment.yml | 22 --- .../resources/conda_test_environment.yml | 14 -- .github/workflows/setup-build-env/action.yml | 2 +- .gitignore | 11 +- CONTRIBUTING.md | 2 +- Changelog.md | 7 + MANIFEST.in | 15 -- README.md | 13 +- conda/conda_build_config.yaml | 4 - conda/meta.yaml | 55 -------- meson.build | 37 +++-- pyproject.toml | 26 ++-- setup.cfg | 11 -- setup.py | 129 ------------------ stripy/Notebooks/0-StartHere.ipynb | 22 +-- stripy/__init__.py | 2 +- stripy/cartesian_meshes.py | 4 +- 23 files changed, 157 insertions(+), 391 deletions(-) delete mode 100644 .github/workflows/resources/conda_api_docs_environment.yml delete mode 100644 .github/workflows/resources/conda_build_environment.yml delete mode 100644 .github/workflows/resources/conda_jb_docs_environment.yml delete mode 100644 .github/workflows/resources/conda_test_environment.yml delete mode 100755 MANIFEST.in delete mode 100644 conda/conda_build_config.yaml delete mode 100644 conda/meta.yaml delete mode 100644 setup.cfg delete mode 100755 setup.py diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 69447a90..2b34e6f3 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -2,38 +2,54 @@ name: Run tests on: push: + pull_request: release: workflow_dispatch: jobs: test_ubuntu: + name: py${{ matrix.python-version }} / ${{ matrix.numpy-spec }} runs-on: ubuntu-latest strategy: + fail-fast: false matrix: - python-version: ["3.9", "3.10", "3.11", "3.12"] - + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] + # Build always happens against NumPy 2.x (build-system.requires), then we + # force the *runtime* NumPy to exercise both the 1.x and 2.x ABI paths. + # This is the exact scenario from gh-114 (numpy-2-built wheel on numpy 1.x). + numpy-spec: ["numpy>=1.19,<2", "numpy>=2"] + exclude: + # NumPy 1.x has no wheels for Python 3.13 + - python-version: "3.13" + numpy-spec: "numpy>=1.19,<2" + steps: - - uses: actions/checkout@v2 - + - uses: actions/checkout@v4 + - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install setuptools wheel twine - pip install numpy - pip install scipy - pip install Cython - - - name: Install - run: | - pip install . - + - name: Set up build environment (Fortran + ninja) + uses: ./.github/workflows/setup-build-env + + - name: Build and install stripy + # Built with PEP 517 isolation -> compiled against NumPy 2.x + run: pip install . + + - name: Pin runtime NumPy and install test deps + # Resolve together so the NumPy pin is honoured (a separate scipy/pytest + # resolve could otherwise pull NumPy back up). + run: pip install "${{ matrix.numpy-spec }}" pytest scipy + + - name: Show resolved versions + run: pip list | grep -iE "numpy|stripy" || true + - name: Run tests + # Run from a neutral directory so the *installed* package (with its + # compiled extensions) is imported, not the ./stripy source-tree shadow. run: | - pip install pytest - pytest -v + cd "${{ runner.temp }}" + python -m pytest -v "${{ github.workspace }}/stripy/tests" diff --git a/.github/workflows/build_deploy_jbdoc.yml b/.github/workflows/build_deploy_jbdoc.yml index 7b1615c5..3b5724e2 100644 --- a/.github/workflows/build_deploy_jbdoc.yml +++ b/.github/workflows/build_deploy_jbdoc.yml @@ -1,51 +1,48 @@ name: API docs / jupyterbook -# We should trigger this from an upload event +# We should trigger this from an upload event on: push: # We publish the beta docs as well - branches: - - master + branches: + - master - dev jobs: deploy: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 with: - python-version: "3.10" + python-version: "3.11" + + - name: Set up build environment (Fortran + ninja) + uses: ./.github/workflows/setup-build-env - - name: Install dependencies + - name: Install stripy and doc dependencies run: | python -m pip install --upgrade pip - pip install setuptools wheel twine - pip install numpy - pip install scipy - pip install Cython - pip install jupyter-book - - - name: Install latest stripy - run: | + pip install scipy jupyter-book pip install . - name: Build docs with jupyterbook shell: bash -l {0} run: | - VERSION=`python setup.py --version` + VERSION=`python -c "from importlib.metadata import version; print(version('stripy'))"` echo "VERSION=$(echo $VERSION)" >> $GITHUB_ENV cd jupyterbook - ./build-book.sh + ./build-book.sh # Note deploy key needs the ----BEGIN ... KEY---- lines too ## Always deploy to version directory - name: Deploy uses: peaceiris/actions-gh-pages@v3 - + with: github_token: ${{ secrets.GITHUB_TOKEN }} destination_dir: ${{ env.VERSION }} publish_dir: jupyterbook/_build/html - - diff --git a/.github/workflows/build_deploy_pdoc.yml b/.github/workflows/build_deploy_pdoc.yml index b958b061..809c8554 100644 --- a/.github/workflows/build_deploy_pdoc.yml +++ b/.github/workflows/build_deploy_pdoc.yml @@ -1,9 +1,9 @@ -name: API docs / pdoc3 +name: API docs / pdoc3 -# We should trigger this from an upload event +# We should trigger this from an upload event on: - push: # This is not really a great idea + push: # This is not really a great idea branches: - master @@ -11,22 +11,20 @@ jobs: deploy: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 with: python-version: "3.11" - - name: Install dependencies + - name: Set up build environment (Fortran + ninja) + uses: ./.github/workflows/setup-build-env + + - name: Install stripy and doc dependencies run: | python -m pip install --upgrade pip - pip install setuptools wheel twine - pip install numpy - pip install scipy - pip install Cython - pip install pdoc3 - pip install pytest - - - name: Install latest stripy - run: | + pip install scipy pdoc3 pip install . - name: Build docs with pdoc @@ -38,15 +36,15 @@ jobs: cd docs pdoc --config latex_math=True --html -o api --force stripy - # Here we can add an action to build the jupyter book to a different directory + # Here we can add an action to build the jupyter book to a different directory # and then they can be uploaded together. The jupyter book can contain a link to the # API docs. # Note deploy key needs the ----BEGIN ... KEY---- lines too - name: Deploy uses: peaceiris/actions-gh-pages@v3 - + with: github_token: ${{ secrets.GITHUB_TOKEN }} destination_dir: docs/ - publish_dir: docs/api/stripy \ No newline at end of file + publish_dir: docs/api/stripy diff --git a/.github/workflows/pypi.yml b/.github/workflows/pypi.yml index aabe4af4..31c8c972 100644 --- a/.github/workflows/pypi.yml +++ b/.github/workflows/pypi.yml @@ -11,13 +11,13 @@ jobs: os: [ubuntu-latest, windows-latest, macos-13, macos-14] steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 with: python-version: 3.x - name: Build wheels - uses: pypa/cibuildwheel@v2.20.0 + uses: pypa/cibuildwheel@v2.21.3 env: # Disable building PyPy wheels on all platforms CIBW_SKIP: 'pp*' @@ -27,8 +27,13 @@ jobs: CIBW_BEFORE_BUILD_WINDOWS: "choco install ninja" CIBW_BEFORE_BUILD_MACOS: "brew install ninja gcc && brew reinstall gcc" # CIBW_BEFORE_BUILD_LINUX: "apt-get install -y ninja-build" - CIBW_BEFORE_BUILD: "pip install numpy meson-python ninja setuptools build" - CIBW_TEST_REQUIRES: pytest + # NumPy is NOT installed here on purpose: cibuildwheel builds with PEP 517 + # isolation, so the compile-time NumPy comes from `build-system.requires` + # in pyproject.toml (numpy>=2.0), giving wheels that run on NumPy 1.x and 2.x. + # A system ninja just lets meson find one without a per-build pip install. + CIBW_BEFORE_BUILD: "pip install ninja" + # scipy is needed by the mesh tests (stripy uses scipy.optimize/spatial lazily) + CIBW_TEST_REQUIRES: pytest scipy CIBW_TEST_COMMAND: "pytest {project}/stripy/tests" # ... # with: @@ -38,6 +43,9 @@ jobs: - uses: actions/upload-artifact@v4 with: + # Unique name per matrix job: upload-artifact@v4 rejects duplicate + # artifact names within a run, so a shared default name would fail the build. + name: cibw-wheels-${{ matrix.os }} path: ./wheelhouse/*.whl build_sdist: @@ -51,6 +59,7 @@ jobs: - uses: actions/upload-artifact@v4 with: + name: cibw-sdist path: dist/*.tar.gz upload_pypi: @@ -63,11 +72,11 @@ jobs: # or, alternatively, upload to PyPI on every tag starting with 'v' (remove on: release above to use this) # if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') steps: - - uses: actions/download-artifact@v4.1.7 + - uses: actions/download-artifact@v4 with: - # unpacks default artifact into dist/ - # if `name: artifact` is omitted, the action will create extra parent dir - name: artifact + # Gather every wheel/sdist artifact from the build jobs into dist/ + pattern: cibw-* + merge-multiple: true path: dist - uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.github/workflows/resources/conda_api_docs_environment.yml b/.github/workflows/resources/conda_api_docs_environment.yml deleted file mode 100644 index cf763ad7..00000000 --- a/.github/workflows/resources/conda_api_docs_environment.yml +++ /dev/null @@ -1,11 +0,0 @@ -# This is the environment needed to build stripy. As this will be cached, we can include any stripy dependencies -# so they are not downloaded again - this should save a bit of time - -name: conda-build-docs -channels: - - conda-forge - - geo-down-under -dependencies: - - numpy>=1.16.0 - - scipy>=1.0.0 - - pdoc3 diff --git a/.github/workflows/resources/conda_build_environment.yml b/.github/workflows/resources/conda_build_environment.yml deleted file mode 100644 index df33c121..00000000 --- a/.github/workflows/resources/conda_build_environment.yml +++ /dev/null @@ -1,13 +0,0 @@ -# This is the environment needed to build stripy. Note, the build is in a new -# environment so all we need here is the tools to run the build in the first place -# not all the stripy dependencies. - -name: conda-build -channels: - - conda-forge -dependencies: - # numpy is needed for some of the setup tools - - numpy - - conda-build - - conda-verify - - anaconda-client diff --git a/.github/workflows/resources/conda_jb_docs_environment.yml b/.github/workflows/resources/conda_jb_docs_environment.yml deleted file mode 100644 index 94429c7c..00000000 --- a/.github/workflows/resources/conda_jb_docs_environment.yml +++ /dev/null @@ -1,22 +0,0 @@ -# This is the environment needed to run the stripy examples / build the jbook - -name: conda-build-docs -channels: - - conda-forge - - geo-down-under -dependencies: - - geo-down-under::stripy==2.0.5b2 # this only works after the upload, of course - - geo-down-under::litho1pt0 - - matplotlib - - pyproj - - pyepsg - - h5netcdf - - netcdf4 - - cartopy - - xarray - - k3d - - pip - - - pip: - - jupyter-book - diff --git a/.github/workflows/resources/conda_test_environment.yml b/.github/workflows/resources/conda_test_environment.yml deleted file mode 100644 index 99e742c3..00000000 --- a/.github/workflows/resources/conda_test_environment.yml +++ /dev/null @@ -1,14 +0,0 @@ -# This is the environment needed to build stripy for pip - -name: conda-test -channels: - - conda-forge -dependencies: - # numpy is needed for some of the setup tools - - compilers - - numpy - - scipy - - cython - - pip - - diff --git a/.github/workflows/setup-build-env/action.yml b/.github/workflows/setup-build-env/action.yml index 6f1f4b4a..a8b4565a 100644 --- a/.github/workflows/setup-build-env/action.yml +++ b/.github/workflows/setup-build-env/action.yml @@ -6,7 +6,7 @@ description: "Install ninja and a fortran compiler" runs: using: "composite" steps: - - uses: awvwgk/setup-fortran@v1 + - uses: fortran-lang/setup-fortran@v1 - run: pip install --upgrade pip shell: bash diff --git a/.gitignore b/.gitignore index 5429b725..784a462c 100755 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,10 @@ __pycache__ .pytest_cache *.png +# Local notebook run outputs / GPlates data caches (not part of the package) +stripy/Notebooks/spherical_grid.* +*.gplates.cache + .coverage swpcom.mod @@ -24,8 +28,11 @@ litho1pt0-src/dist/* *.egg .eggs -src/_*module.c -src/_*wrappers.f +# f2py-generated extension sources (the historical in-root location and src/) +/_*module.c +/_*-f2pywrappers.f +src/*module.c +src/*wrappers.f .local .ipython diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e47bcc87..3900c7c2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -26,7 +26,7 @@ It is a good idea to commit frequently and with commit messages that refer to th We use `pytest` for the unit testing framework in stripy. In the source directory this means running: ```bash -python setup.py test +pytest ``` The existing tests should be passing before you start coding (help us out with an issue if that is not the case !) and when you have finished. Any new functionality should also have tests that we can use to verify the code. It is important that you make it clear if the original tests have had to change to accomodate new code / functionality. diff --git a/Changelog.md b/Changelog.md index 1a5540d9..07338af5 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,5 +1,12 @@ # Stripy changelog +## 2.3.4 + + - Build against NumPy 2.x so wheels run on both NumPy 1.x and 2.x at runtime ([#114](https://github.com/underworldcode/stripy/issues/114)). + - Replace NumPy aliases removed in NumPy 1.24 (`np.int`, `np.bool`) for NumPy 2.x compatibility. + - Fix the release wheel workflow for `actions/upload-artifact@v4` (unique per-job artifact names). + - Meson build robustness fixes and CI modernisation; remove the obsolete `numpy.distutils` `setup.py` build path. + ## 2.1.0 - Jupyterbook diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100755 index 039e22f1..00000000 --- a/MANIFEST.in +++ /dev/null @@ -1,15 +0,0 @@ -include README.md -include MANIFEST.in -include LICENCE -include CONTRIBUTING.md -include src/* -include stripy/__init__.py -include stripy/cartesian.py -include stripy/spherical.py -include stripy/spherical_meshes.py -include stripy/cartesian_meshes.py -include stripy/documentation.py -include stripy/Notebooks/SphericalTriangulations/*.ipynb -include stripy/Notebooks/CartesianTriangulations/*.ipynb -include stripy/Notebooks/Data/* -include stripy/Notebooks/0-StartHere.ipynb diff --git a/README.md b/README.md index a1367bfe..e3bbaa4b 100755 --- a/README.md +++ b/README.md @@ -3,9 +3,8 @@ [![Docker Cloud Automated build](https://img.shields.io/docker/cloud/automated/underworldcode/stripy.svg)](https://hub.docker.com/r/underworldcode/stripy) [![PyPI](https://img.shields.io/pypi/v/stripy.svg)](https://pypi.org/project/stripy/) -[![pip builds](https://github.com/underworldcode/stripy/workflows/pip%20builds/badge.svg)](https://github.com/underworldcode/stripy/actions?query=workflow%3A%22pip+builds%22) - -![Conda Deployment](https://github.com/underworldcode/stripy/workflows/Conda%20Deployment/badge.svg) +[![Build wheels](https://github.com/underworldcode/stripy/actions/workflows/pypi.yml/badge.svg)](https://github.com/underworldcode/stripy/actions/workflows/pypi.yml) +[![Run tests](https://github.com/underworldcode/stripy/actions/workflows/CI.yml/badge.svg)](https://github.com/underworldcode/stripy/actions/workflows/CI.yml) A Python interface to TRIPACK and STRIPACK Fortran code for (constrained) triangulation in Cartesian coordinates and on a sphere. Stripy is an object-oriented package and includes routines from SRFPACK and SSRFPACK for interpolation (nearest neighbor, linear and hermite cubic) and to evaluate derivatives (Renka 1996a,b and 1997a,b). @@ -145,16 +144,16 @@ All the dependencies will be automatically installed by `pip`, except for `gfort (or any Fortran compiler). It must be installed in your system before installing `stripy` with `pip`. -If you change the Fortran compiler, you may have to add the -flags `config_fc --fcompiler=` when `setup.py` is run -(see docs for [numpy.distutils](http://docs.scipy.org/doc/numpy-dev/f2py/distutils.html)). +The Fortran extensions are built with [meson-python](https://meson-python.readthedocs.io). +If you need to select a specific Fortran compiler, set the standard `FC` environment +variable (e.g. `FC=gfortran-13`) before invoking `pip install`. ### Installing with conda If you use the anaconda packaging system, then you should be able to ```bash -conda install -c geo-down-under stripy +conda install -c conda-forge stripy ``` ## Usage diff --git a/conda/conda_build_config.yaml b/conda/conda_build_config.yaml deleted file mode 100644 index b0fe25b7..00000000 --- a/conda/conda_build_config.yaml +++ /dev/null @@ -1,4 +0,0 @@ -fortran_compiler: # [unix or win64] - - gfortran # [(linux64 or osx)] - - gfortran # [aarch64 or ppc64le or armv7l] - - flang # [win64] diff --git a/conda/meta.yaml b/conda/meta.yaml deleted file mode 100644 index bb35cf0e..00000000 --- a/conda/meta.yaml +++ /dev/null @@ -1,55 +0,0 @@ -{% set name = "stripy" %} -{% set data = load_setup_py_data() %} - -package: - name: "{{ name|lower }}" - version: {{ data.get('version') }} - -source: - path: .. - -build: - number: 0 - script: "{{ PYTHON }} -m pip install . -vv" - -requirements: - - host: - - python - - pip - - numpy >=1.16.0 - - scipy >=1.0.0 - - {{ compiler('fortran')}} - - {{ compiler('c')}} - - run: - - python - - numpy >=1.16.0 - - scipy >=1.0.0 - -test: - requires: - - pytest - - source_files: - - stripy/tests - - commands: - - pytest - - - -about: - home: "https://github.com/underworldcode/stripy" - license: LGPL - license_family: GPL - license_file: ../COPYING.LESSER - summary: "Python interface to TRIPACK and STRIPACK fortran code for triangulation/interpolation in Cartesian coordinates and on a sphere" - doc_url: - dev_url: - -extra: - recipe-maintainers: - - lmoresi - - brmather - - rbeucher diff --git a/meson.build b/meson.build index bad6db72..7916ff3e 100644 --- a/meson.build +++ b/meson.build @@ -8,9 +8,9 @@ platform = host_machine.system() if platform == 'windows' add_project_link_arguments('-static', language: ['fortran', 'c']) elif platform == 'darwin' - add_project_link_arguments('-Wl,-rpath, "@loader_path"', language: ['fortran', 'c']) + add_project_link_arguments('-Wl,-rpath,@loader_path', language: ['fortran', 'c']) else - add_project_link_arguments('-Wl,-rpath,"$ORIGIN"', language: ['fortran', 'c']) + add_project_link_arguments('-Wl,-rpath,$ORIGIN', language: ['fortran', 'c']) endif py_mod = import('python') @@ -18,45 +18,60 @@ py = py_mod.find_installation(pure: false) py_dep = py.dependency() incdir_numpy = run_command(py, - ['-c', 'import os; os.chdir(".."); import numpy; print(numpy.get_include())'], + ['-c', 'import numpy; print(numpy.get_include())'], check : true ).stdout().strip() incdir_f2py = run_command(py, - ['-c', 'import os; os.chdir(".."); import numpy.f2py; print(numpy.f2py.get_include())'], - check : true + ['-c', 'import numpy.f2py; print(numpy.f2py.get_include())'], + check : true ).stdout().strip() inc_np = include_directories(incdir_numpy, incdir_f2py) -install_subdir(name, install_dir: py.get_install_dir() / name, strip_directory: true) +# Install the pure-Python package, including the shipped example Notebooks that +# stripy.documentation relies on. The excludes drop common editor/interpreter +# cruft; note meson matches exact relative paths (no globbing), so these only +# catch top-level occurrences. Clean release wheels are built by CI from a fresh +# checkout, so this is just a best-effort safety net for local (dirty) installs. +install_subdir(name, + install_dir: py.get_install_dir() / name, + strip_directory: true, + exclude_files: ['.DS_Store'], + exclude_directories: ['__pycache__', '.ipynb_checkpoints'], +) +# f2py turns each .pyf interface into a C extension source (module.c) and, +# when the interface requires it, a Fortran wrapper (-f2pywrappers.f). The +# `output` lists below must match exactly what the pinned f2py (numpy>=2.0) +# emits; if a future f2py changes its output set, update these lists to match or +# the build will fail with a missing/unexpected output error. tripack_source = custom_target('_tripackmodule.c', - input : ['src/tripack.pyf'], # .f so no F90 wrappers + input : ['src/tripack.pyf'], output : ['_tripackmodule.c', '_tripack-f2pywrappers.f'], command : [py, '-m', 'numpy.f2py', '@INPUT@'] ) stripack_source = custom_target('_stripackmodule.c', - input : ['src/stripack.pyf'], # .f so no F90 wrappers + input : ['src/stripack.pyf'], output : ['_stripackmodule.c', '_stripack-f2pywrappers.f'], command : [py, '-m', 'numpy.f2py', '@INPUT@'] ) srfpack_source = custom_target('_srfpackmodule.c', - input : ['src/srfpack.pyf'], # .f so no F90 wrappers + input : ['src/srfpack.pyf'], output : ['_srfpackmodule.c'], command : [py, '-m', 'numpy.f2py', '@INPUT@'] ) ssrfpack_source = custom_target('_ssrfpackmodule.c', - input : ['src/ssrfpack.pyf'], # .f so no F90 wrappers + input : ['src/ssrfpack.pyf'], output : ['_ssrfpackmodule.c', '_ssrfpack-f2pywrappers.f'], command : [py, '-m', 'numpy.f2py', '@INPUT@'] ) fortran_source = custom_target('_fortranmodule.c', - input : ['src/stripyf.pyf'], # .f so no F90 wrappers + input : ['src/stripyf.pyf'], output : ['_fortranmodule.c', '_fortran-f2pywrappers.f'], command : [py, '-m', 'numpy.f2py', '@INPUT@'] ) diff --git a/pyproject.toml b/pyproject.toml index 38ecafff..7a423719 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,11 +1,11 @@ [build-system] build-backend = "mesonpy" requires = [ - "numpy>=2.0", - "pip", "meson-python>=0.15.0", - "setuptools>=61.0", - "build", + # Build against NumPy 2.x: the resulting binaries are compatible with + # both NumPy 1.x and 2.x at runtime. Building against 1.x is NOT + # forward-compatible with 2.x, which is the cause of gh-114. + "numpy>=2.0.0", ] [project] @@ -29,24 +29,16 @@ classifiers = [ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", ] keywords = ["triangulation", "sphere", "interpolation", "mesh"] dependencies = [ - "numpy>=2.0.0", + # Runtime requirement is intentionally looser than the build requirement: + # wheels built against NumPy 2.x remain ABI-compatible with older NumPy + # at runtime, back to the 2.0 backward-compat floor of 1.19.3 (see gh-114). + "numpy>=1.19.3", ] -[tool.setuptools] -packages = [ - "stripy", - "stripy._tripack", - "stripy._stripack", - "stripy._srfpack", - "stripy._ssrfpack", -] - -# [tool.setuptools.dynamic] -# version = {attr = "stripy.__version__"} - [project.optional-dependencies] dev = ["pip-tools", "pytest", "scipy>=1.0.0"] diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 3712537d..00000000 --- a/setup.cfg +++ /dev/null @@ -1,11 +0,0 @@ -[aliases] -test=pytest - -[bdist_wheel] - -# This flag says that the code is written to work on both Python 2 and Python -# 3. If at all possible, it is good practice to do this. If you cannot, you -# will need to generate wheels for each Python version that you support. - -[metadata] -description-file = README.md diff --git a/setup.py b/setup.py deleted file mode 100755 index 8330d62f..00000000 --- a/setup.py +++ /dev/null @@ -1,129 +0,0 @@ -# To install locally: python setup.py build && python setup.py install -# (If there are problems with installation of the documentation, the -# egg file may be out of sync and will need to be manually deleted -# - see error message for details of the corrupted zip file. ) -# -# To push a version through to pip. -# - Make sure it installs correctly locally as above -# - Update the version information in this file -# - python setup.py sdist upload -r pypitest # for the test version -# - python setup.py sdist upload -r pypi # for the real version -# With twine: -# - python setup.py sdist -# - twine upload dist/* -# -# (see http://peterdowns.com/posts/first-time-with-pypi.html) - -from setuptools import setup -from numpy.distutils.core import setup, Extension - -# try: -# from distutils.command import bdist_conda -# except ImportError: -# pass - -from os import path -import io -import os -import subprocess -import platform - -link_args = ['-Wl,-rpath,"$ORIGIN"'] - -if "Windows" in platform.system(): - link_args = ["-static"] - -if "Darwin" in platform.system(): - link_args = ['-Wl,-rpath, "@loader_path"'] - - -# in development set version to none and ... -PYPI_VERSION = "2.3.4" # Note: don't add any dashes if you want to use conda, use b1 not .b1 - -# Return the git revision as a string (from numpy) - -def git_version(): - - def _minimal_ext_cmd(cmd): - # construct minimal environment - env = {} - for k in ['SYSTEMROOT', 'PATH']: - v = os.environ.get(k) - if v is not None: - env[k] = v - # LANGUAGE is used on win32 - env['LANGUAGE'] = 'C' - env['LANG'] = 'C' - env['LC_ALL'] = 'C' - out = subprocess.Popen(cmd, stdout = subprocess.PIPE, env=env).communicate()[0] - return out - - try: - out = _minimal_ext_cmd(['git', 'rev-parse', '--short', 'HEAD']) - GIT_REVISION = out.strip().decode('ascii') - except OSError: - GIT_REVISION = "Unknown" - - return GIT_REVISION - - -if PYPI_VERSION is None: - PYPI_VERSION = git_version() - - -this_directory = path.abspath(path.dirname(__file__)) -with io.open(path.join(this_directory, 'README.md'), encoding='utf-8') as f: - long_description = f.read() - -# What works on github does not work elsewhere -long_description = long_description.replace('(stripy/Notebooks','(https://github.com/underworldcode/stripy/blob/master/stripy/Notebooks') - -# print(long_description) - -# interface for Renka's algorithm 772 fortran code -ext1 = Extension(name = 'stripy._stripack', - sources = ['src/stripack.pyf','src/stripack.f90'], - extra_link_args=link_args) -ext2 = Extension(name = 'stripy._tripack', - sources = ['src/tripack.pyf', 'src/tripack.f90'], - extra_link_args=link_args) -ext3 = Extension(name = 'stripy._srfpack', - sources = ['src/srfpack.pyf', 'src/srfpack.f'], - extra_link_args=link_args) -ext4 = Extension(name = 'stripy._ssrfpack', - sources = ['src/ssrfpack.pyf', 'src/ssrfpack.f'], - extra_link_args=link_args) -ext5 = Extension(name = 'stripy._fortran', - sources = ['src/stripyf.pyf', 'src/stripyf.f90'], - extra_link_args=link_args) - - -if __name__ == "__main__": - setup(name = 'stripy', - author = "Louis Moresi", - author_email = "louis.moresi@anu.edu.au", - url = "https://github.com/underworldcode/stripy", - version = PYPI_VERSION, - description = "Python interface to TRIPACK and STRIPACK fortran code for triangulation/interpolation in Cartesian coordinates and on a sphere", - long_description = long_description, - long_description_content_type='text/markdown', - ext_modules = [ext1, ext2, ext3, ext4, ext5], - install_requires = ['numpy>=1.16.0'], - packages = ['stripy'], - package_data = {'stripy': ['Notebooks/*ipynb', # Worked Examples is not currently used - 'Notebooks/CartesianTriangulations/*ipynb', - 'Notebooks/SphericalTriangulations/*ipynb', - 'Notebooks/Data/*'] }, - include_package_data = True, - classifiers = ['Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3.3', - 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - 'Programming Language :: Python :: 3.10', - 'Programming Language :: Python :: 3.11' - ] - ) diff --git a/stripy/Notebooks/0-StartHere.ipynb b/stripy/Notebooks/0-StartHere.ipynb index 8be50f23..8135cbeb 100644 --- a/stripy/Notebooks/0-StartHere.ipynb +++ b/stripy/Notebooks/0-StartHere.ipynb @@ -74,25 +74,25 @@ "\n", "## Installation\n", "\n", - "To install ([numpy](http://numpy.org) and fortran compiler, preferably\n", - "[gfortran](https://gcc.gnu.org/wiki/GFortran), required):\n", + "`stripy` requires [numpy](http://numpy.org) and a Fortran compiler, preferably\n", + "[gfortran](https://gcc.gnu.org/wiki/GFortran). The Fortran extensions are built\n", + "with [meson-python](https://meson-python.readthedocs.io).\n", + "\n", + "Install the released version with pip:\n", "\n", "```bash\n", - "python setup.py build\n", - "```\n", - " - If you change the fortran compiler, you may have to add the\n", - "flags `config_fc --fcompiler=` when setup.py is run\n", - "(see docs for [numpy.distutils](http://docs.scipy.org/doc/numpy-dev/f2py/distutils.html)).\n", - "```bash\n", - "python setup.py install\n", + "pip install [--user] stripy\n", "```\n", "\n", - "Alternatively install using pip:\n", + "Or build and install from a source checkout:\n", "\n", "```bash\n", - "pip install [--user] stripy\n", + "pip install .\n", "```\n", "\n", + "To select a specific Fortran compiler, set the standard `FC` environment\n", + "variable (e.g. `FC=gfortran-13`) before invoking `pip install`.\n", + "\n", "## Usage\n", "\n", "Two classes are included as part of the Stripy package:\n", diff --git a/stripy/__init__.py b/stripy/__init__.py index 721a6777..77ad37b0 100644 --- a/stripy/__init__.py +++ b/stripy/__init__.py @@ -74,7 +74,7 @@ def weighted_average_to_nodes(x1, x2, data, interpolator ): gridded_data = np.zeros(interpolator.npoints) norm = np.zeros(interpolator.npoints) - count = np.zeros(interpolator.npoints, dtype=np.int) + count = np.zeros(interpolator.npoints, dtype=int) bcc, nodes = interpolator.containing_simplex_and_bcc(x1, x2) diff --git a/stripy/cartesian_meshes.py b/stripy/cartesian_meshes.py index 9e83063e..0cec7bef 100644 --- a/stripy/cartesian_meshes.py +++ b/stripy/cartesian_meshes.py @@ -252,7 +252,7 @@ def equal_angles_in_ellipse(arc_length, a, b): pointsx, pointsy = equal_angles_in_ellipse( spacing, a, b) - bmask = np.full_like(pointsx, 0, dtype=np.bool) + bmask = np.full_like(pointsx, 0, dtype=bool) a -= spacing b -= spacing @@ -260,7 +260,7 @@ def equal_angles_in_ellipse(arc_length, a, b): points = equal_angles_in_ellipse( spacing, a, b) pointsx = np.append(pointsx,points[0]) pointsy = np.append(pointsy,points[1]) - bmask = np.append(bmask, np.full_like(points[0], 1, dtype=np.bool)) + bmask = np.append(bmask, np.full_like(points[0], 1, dtype=bool)) a -= spacing b -= spacing From 031477fe415d04e1071d6a3a7178376f10028ce1 Mon Sep 17 00:00:00 2001 From: Ben Mather Date: Thu, 23 Jul 2026 11:58:32 +1000 Subject: [PATCH 2/9] meson: only emit GNU link flags for a GNU Fortran compiler (refs #116) The per-platform link arguments (`-static` on Windows, `-Wl,-rpath,...` on Unix) are gfortran/GCC conventions. LLVM flang and the MSVC-based linkers warn about or silently ignore them (see gh-116, building with flang 21 on Windows), so gate the whole block on `meson.get_compiler('fortran').get_id() == 'gcc'`. Both `host_machine.system()` and `get_compiler('fortran')` refer to the host/target machine, so cross-compiled gfortran builds (e.g. conda-forge linux-aarch64 / osx-arm64) still receive the correct per-target flags, while non-GNU toolchains no longer get bogus flags. No behaviour change for the existing gfortran Linux / macOS / MinGW (PyPI) builds. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01FFBEqxMhk9Ut6vVm5jfV8d --- meson.build | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/meson.build b/meson.build index 7916ff3e..4a5a32e6 100644 --- a/meson.build +++ b/meson.build @@ -5,12 +5,27 @@ name = 'stripy' add_languages('fortran') platform = host_machine.system() -if platform == 'windows' - add_project_link_arguments('-static', language: ['fortran', 'c']) -elif platform == 'darwin' - add_project_link_arguments('-Wl,-rpath,@loader_path', language: ['fortran', 'c']) -else - add_project_link_arguments('-Wl,-rpath,$ORIGIN', language: ['fortran', 'c']) + +# These link arguments make each extension module self-contained, but they are +# GNU/gfortran conventions. Other Fortran toolchains (LLVM flang, Intel, the +# MSVC-based linkers) warn about, reject, or silently ignore them and provide +# their runtime libraries differently, so only emit them for a GNU (gfortran/GCC) +# compiler (see gh-116). Non-GNU builds rely on the packaging step +# (delocate/auditwheel, or conda compiler activation) to resolve the runtime. +# +# This stays correct under cross-compilation: both host_machine.system() and +# meson.get_compiler('fortran') refer to the *host* (target) machine, so a cross +# build with a gfortran cross-compiler (e.g. conda-forge linux-aarch64 or +# osx-arm64) still selects the right per-target flags. +if meson.get_compiler('fortran').get_id() == 'gcc' + if platform == 'windows' + # Statically link libgfortran/libquadmath (MinGW) so the .pyd is standalone. + add_project_link_arguments('-static', language: ['fortran', 'c']) + elif platform == 'darwin' + add_project_link_arguments('-Wl,-rpath,@loader_path', language: ['fortran', 'c']) + else + add_project_link_arguments('-Wl,-rpath,$ORIGIN', language: ['fortran', 'c']) + endif endif py_mod = import('python') From ed1af11398bf4e7ad8081d4d76e22dc7a572720a Mon Sep 17 00:00:00 2001 From: Ben Mather Date: Thu, 23 Jul 2026 12:03:27 +1000 Subject: [PATCH 3/9] Clarify the ier=-2 collinear-nodes message (closes #117) trmesh (spherical) and its planar counterpart both return ier=-2 in two distinct situations: the first three nodes being collinear, and a later ADDNOD call reporting that all nodes up to some node are collinear (stripack.f90:59,5406 / tripack.f90:421,6433). The old message only described the first case, giving misleading guidance for the second. Extend the _ier_codes[-2] message in both spherical.py and cartesian.py to cover both cases and note the data may be genuinely (near-)collinear or degenerate. (Parsing the Fortran stdout to distinguish the two, as suggested in the issue, isn't reliable through f2py, so a clearer single message is the robust fix.) Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01FFBEqxMhk9Ut6vVm5jfV8d --- stripy/cartesian.py | 6 +++++- stripy/spherical.py | 7 ++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/stripy/cartesian.py b/stripy/cartesian.py index b071cb2e..7513e1a7 100644 --- a/stripy/cartesian.py +++ b/stripy/cartesian.py @@ -29,7 +29,11 @@ _ier_codes = {0: "no errors were encountered.", -1: "N < 3 on input.", - -2: "the first three nodes are collinear.\nSet permute to True or reorder nodes manually.", + -2: "collinear nodes were encountered: either the first three " + "nodes lie on a common line, or TRMESH's incremental insertion " + "(ADDNOD) found all nodes up to some node to be collinear.\n" + "Set permute=True or reorder the nodes manually; if the error " + "persists the points may be (near-)collinear or degenerate.", -3: "duplicate nodes were encountered.", -4: "an error flag was returned by a call to SWAP in ADDNOD.\n \ This is an internal error and should be reported to the programmer.", diff --git a/stripy/spherical.py b/stripy/spherical.py index b3ffcc00..a8ec33b8 100755 --- a/stripy/spherical.py +++ b/stripy/spherical.py @@ -29,7 +29,12 @@ _ier_codes = {0: "no errors were encountered.", -1: "N < 3 on input.", - -2: "the first three nodes lie on a great circle.\nSet permute to True or reorder nodes manually.", + -2: "collinear nodes were encountered: either the first three " + "nodes lie on a common great circle, or STRMESH's incremental " + "insertion (ADDNOD) found all nodes up to some node to be " + "collinear.\nSet permute=True or reorder the nodes manually; if " + "the error persists the points may be (near-)collinear or " + "degenerate on the sphere.", -3: "duplicate nodes were encountered.", -4: "an error flag was returned by a call to SWAP in ADDNOD.\n \ This is an internal error and should be reported to the programmer.", From a68433ab625e43710dca59da3e83fa57c6769973 Mon Sep 17 00:00:00 2001 From: Ben Mather Date: Thu, 23 Jul 2026 12:28:00 +1000 Subject: [PATCH 4/9] tests: tolerance for linear-interpolation endpoints (refs #115) test_linear_interpolation compared interpolated endpoint values with exact float equality, which is platform-dependent for the permuted (random) mesh: it failed on Linux x86_64 under NumPy 2.x while passing on macOS/arm64 (0 failures in 1000 local runs). The endpoints coincide with input nodes, so linear interpolation recovers their coordinate only up to floating-point rounding (~1e-16) -- the test's own comment already noted "machine precision may differ". Compare with np.isclose and emit an informative failure message. The monotonic `ascending` check is unchanged. Applied to both the spherical and Cartesian tests. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01FFBEqxMhk9Ut6vVm5jfV8d --- stripy/tests/test_Triangulation.py | 13 ++++++++----- stripy/tests/test_sTriangulation.py | 13 ++++++++----- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/stripy/tests/test_Triangulation.py b/stripy/tests/test_Triangulation.py index aa6b15ff..07aae9c0 100644 --- a/stripy/tests/test_Triangulation.py +++ b/stripy/tests/test_Triangulation.py @@ -50,13 +50,16 @@ def test_linear_interpolation(permute): # but machine precision may differ so we don't test it # print((Zi == ix).all()) - bounded = Zi[0] == ix[0] and Zi[-1] == ix[-1] + # The endpoints coincide with input nodes, so linear interpolation should + # recover their x. Compare with a tolerance rather than exact equality: for + # a permuted mesh the result differs at the ~1e-16 level and exact equality + # is platform-dependent (see gh-115). The interior is not checked because + # machine precision may differ. + bounded = np.isclose(Zi[0], ix[0]) and np.isclose(Zi[-1], ix[-1]) ascending = ( np.diff(Zi) > 0 ).all() - if bounded and ascending: - print("PASS! (Interpolation - linear") - else: - assert False, "FAIL! (Interpolation - linear)" + assert bounded and ascending, \ + "FAIL! (Interpolation - linear): Zi={}, ix={}, ierr={}".format(Zi, ix, ierr) @pytest.mark.parametrize("permute", [False, True]) diff --git a/stripy/tests/test_sTriangulation.py b/stripy/tests/test_sTriangulation.py index da53cfac..f116e848 100644 --- a/stripy/tests/test_sTriangulation.py +++ b/stripy/tests/test_sTriangulation.py @@ -59,14 +59,17 @@ def test_linear_interpolation(permute): # but machine precision may differ so we don't test it # print((Zi == ilons).all()) - bounded = Zi[0] == ilons[0] and Zi[-1] == ilons[-1] + # The endpoints coincide with input nodes, so linear interpolation should + # recover their longitude. Compare with a tolerance rather than exact + # equality: for a permuted mesh the result differs at the ~1e-16 level and + # exact equality is platform-dependent (see gh-115). The interior is not + # checked because machine precision may differ. + bounded = np.isclose(Zi[0], ilons[0]) and np.isclose(Zi[-1], ilons[-1]) ascending = ( np.diff(Zi) > 0 ).all() # check if linear gradient across the equator - if bounded and ascending: - print("PASS! (Interpolation - linear") - else: - assert False, "FAIL! (Interpolation - linear)" + assert bounded and ascending, \ + "FAIL! (Interpolation - linear): Zi={}, ilons={}, ierr={}".format(Zi, ilons, ierr) @pytest.mark.parametrize("permute", [False, True]) From 7c0e626fe089aaa1e08e01e78c01719703536ce4 Mon Sep 17 00:00:00 2001 From: Ben Mather Date: Thu, 23 Jul 2026 12:28:00 +1000 Subject: [PATCH 5/9] pypi: set MACOSX_DEPLOYMENT_TARGET per macOS runner for delocate The macos-13/14 wheel jobs failed in delocate with "Library dependencies do not satisfy target MacOS version 11.0": Homebrew's gfortran targets the runner's macOS version, so the libgfortran/libquadmath that delocate bundles require that minimum, above the wheel's default 11.0 tag. Match MACOSX_DEPLOYMENT_TARGET to the runner (13.0 / 14.0) via CIBW_ENVIRONMENT_MACOS. Also set fail-fast: false so one OS failing no longer cancels and masks the others. Note: this raises the wheels' minimum macOS (arm64 -> 14, x86_64 -> 13); a portable gfortran targeting an older macOS could lower it later. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01FFBEqxMhk9Ut6vVm5jfV8d --- .github/workflows/pypi.yml | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pypi.yml b/.github/workflows/pypi.yml index 31c8c972..d8bf9b5c 100644 --- a/.github/workflows/pypi.yml +++ b/.github/workflows/pypi.yml @@ -7,8 +7,16 @@ jobs: name: Build wheels on ${{ matrix.os }} runs-on: ${{ matrix.os }} strategy: + # Don't let one OS failing cancel (and mask) the others. + fail-fast: false matrix: - os: [ubuntu-latest, windows-latest, macos-13, macos-14] + include: + - os: ubuntu-latest + - os: windows-latest + - os: macos-13 + macos_target: "13.0" + - os: macos-14 + macos_target: "14.0" steps: - uses: actions/checkout@v4 @@ -22,6 +30,13 @@ jobs: # Disable building PyPy wheels on all platforms CIBW_SKIP: 'pp*' CIBW_ENVIRONMENT_WINDOWS: "CFLAGS='-lquadmath'" + # Homebrew's gfortran on the macos-13/14 runners targets the runner's + # macOS version, so the libgfortran/libquadmath that delocate bundles + # require that minimum. Match MACOSX_DEPLOYMENT_TARGET to the runner or + # delocate rejects the wheel ("Library dependencies do not satisfy + # target MacOS version"). (This does raise the wheel's minimum macOS; + # a portable gfortran targeting an older macOS could lower it later.) + CIBW_ENVIRONMENT_MACOS: "MACOSX_DEPLOYMENT_TARGET=${{ matrix.macos_target }}" CIBW_PROJECT_REQUIRES_PYTHON: ">=3.9" CIBW_ARCHS: 'auto64' CIBW_BEFORE_BUILD_WINDOWS: "choco install ninja" From 1e37966d999d407db9cb58282d5b95627c7c6627 Mon Sep 17 00:00:00 2001 From: Ben Mather Date: Thu, 23 Jul 2026 13:51:25 +1000 Subject: [PATCH 6/9] pypi: install test-time scipy from wheels only (fix Linux wheel test) Adding scipy to CIBW_TEST_REQUIRES caused the manylinux wheel test to fail: for some interpreters pip resolved a scipy version with no matching wheel (e.g. scipy 1.17.1 for cp311) and tried to build it from the sdist inside the manylinux container, which has no OpenBLAS/build tooling ("Dependency OpenBLAS not found"). Set PIP_ONLY_BINARY=scipy on all three platforms so pip only installs a scipy wheel, falling back to the newest version that ships one for the target interpreter. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01FFBEqxMhk9Ut6vVm5jfV8d --- .github/workflows/pypi.yml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pypi.yml b/.github/workflows/pypi.yml index d8bf9b5c..d84dcadb 100644 --- a/.github/workflows/pypi.yml +++ b/.github/workflows/pypi.yml @@ -29,14 +29,20 @@ jobs: env: # Disable building PyPy wheels on all platforms CIBW_SKIP: 'pp*' - CIBW_ENVIRONMENT_WINDOWS: "CFLAGS='-lquadmath'" + # PIP_ONLY_BINARY=scipy: install the test-time scipy from a wheel only. + # Otherwise pip may pick a scipy version with no wheel for the target + # interpreter and try to build it from source, which fails in the + # manylinux container (no OpenBLAS/build tooling). Forcing wheels makes + # pip fall back to the newest scipy that ships one. + CIBW_ENVIRONMENT_LINUX: "PIP_ONLY_BINARY=scipy" + CIBW_ENVIRONMENT_WINDOWS: "CFLAGS='-lquadmath' PIP_ONLY_BINARY=scipy" # Homebrew's gfortran on the macos-13/14 runners targets the runner's # macOS version, so the libgfortran/libquadmath that delocate bundles # require that minimum. Match MACOSX_DEPLOYMENT_TARGET to the runner or # delocate rejects the wheel ("Library dependencies do not satisfy # target MacOS version"). (This does raise the wheel's minimum macOS; # a portable gfortran targeting an older macOS could lower it later.) - CIBW_ENVIRONMENT_MACOS: "MACOSX_DEPLOYMENT_TARGET=${{ matrix.macos_target }}" + CIBW_ENVIRONMENT_MACOS: "MACOSX_DEPLOYMENT_TARGET=${{ matrix.macos_target }} PIP_ONLY_BINARY=scipy" CIBW_PROJECT_REQUIRES_PYTHON: ">=3.9" CIBW_ARCHS: 'auto64' CIBW_BEFORE_BUILD_WINDOWS: "choco install ninja" From 0bcb6d7c177ce28a2d49f68c4cb5b1c96aa7641c Mon Sep 17 00:00:00 2001 From: Ben Mather Date: Thu, 23 Jul 2026 13:53:44 +1000 Subject: [PATCH 7/9] ci: add concurrency cancel + stop redundant feature-branch wheel builds Add a concurrency group (cancel-in-progress) to both workflows so a new push cancels the superseded run on the same ref instead of leaving stale jobs queued behind the slow macOS runners. Also restrict the wheel-build push trigger to master; feature-branch commits are already validated by the pull_request event, so this stops every branch push from redundantly building the full 4-OS wheel matrix. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01FFBEqxMhk9Ut6vVm5jfV8d --- .github/workflows/CI.yml | 5 +++++ .github/workflows/pypi.yml | 13 ++++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 2b34e6f3..d4d1eafb 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -6,6 +6,11 @@ on: release: workflow_dispatch: +# Cancel superseded runs on the same ref so stale jobs don't pile up. +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: test_ubuntu: diff --git a/.github/workflows/pypi.yml b/.github/workflows/pypi.yml index d84dcadb..9d06df78 100644 --- a/.github/workflows/pypi.yml +++ b/.github/workflows/pypi.yml @@ -1,6 +1,17 @@ name: Build wheels -on: [push,pull_request,release,workflow_dispatch] +on: + push: + branches: [master] # feature branches are covered by the pull_request event + pull_request: + release: + workflow_dispatch: + +# Cancel superseded runs on the same ref so stale wheel builds don't pile up in +# the (slow) macOS runner queue. +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true jobs: build_wheels: From 7e763126cc806b5bec1711ee6d8e69c60b468554 Mon Sep 17 00:00:00 2001 From: Ben Mather Date: Thu, 23 Jul 2026 13:58:20 +1000 Subject: [PATCH 8/9] ci: trim test matrix and drop Intel-mac wheels Run tests (CI.yml): 9 jobs -> 3. Its unique coverage is the NumPy-1.x runtime ABI path (gh-114), so test that on the oldest and a recent Python plus one NumPy-2.x source-build smoke test on the newest. Per-OS / per-Python NumPy-2.x coverage is already provided by the wheel build+test in pypi.yml, so the previous 9-way python x numpy grid was largely redundant. Build wheels: drop the macos-13 (x86_64) job and ship Apple-silicon (arm64) wheels only. Intel-mac users are served by the conda-forge package; x86_64 was also the slowest job to get a runner. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01FFBEqxMhk9Ut6vVm5jfV8d --- .github/workflows/CI.yml | 20 ++++++++++++-------- .github/workflows/pypi.yml | 5 ++--- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index d4d1eafb..a1d6de32 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -18,16 +18,20 @@ jobs: runs-on: ubuntu-latest strategy: fail-fast: false + # This workflow's unique job is to prove a NumPy-2-built package runs on + # NumPy 1.x (the gh-114 ABI path) from a source install. Per-OS / per-Python + # coverage with NumPy 2.x is already provided by the wheel build+test in + # pypi.yml, so we only need a small representative matrix here: + # - NumPy 1.x runtime on the oldest and a recent Python (the ABI path) + # - one NumPy 2.x source-build smoke test on the newest Python matrix: - python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] - # Build always happens against NumPy 2.x (build-system.requires), then we - # force the *runtime* NumPy to exercise both the 1.x and 2.x ABI paths. - # This is the exact scenario from gh-114 (numpy-2-built wheel on numpy 1.x). - numpy-spec: ["numpy>=1.19,<2", "numpy>=2"] - exclude: - # NumPy 1.x has no wheels for Python 3.13 - - python-version: "3.13" + include: + - python-version: "3.9" + numpy-spec: "numpy>=1.19,<2" + - python-version: "3.12" numpy-spec: "numpy>=1.19,<2" + - python-version: "3.13" + numpy-spec: "numpy>=2" steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/pypi.yml b/.github/workflows/pypi.yml index 9d06df78..c2bc4682 100644 --- a/.github/workflows/pypi.yml +++ b/.github/workflows/pypi.yml @@ -24,8 +24,7 @@ jobs: include: - os: ubuntu-latest - os: windows-latest - - os: macos-13 - macos_target: "13.0" + # Apple-silicon (arm64) only; Intel-mac (x86_64) users are served by conda. - os: macos-14 macos_target: "14.0" @@ -47,7 +46,7 @@ jobs: # pip fall back to the newest scipy that ships one. CIBW_ENVIRONMENT_LINUX: "PIP_ONLY_BINARY=scipy" CIBW_ENVIRONMENT_WINDOWS: "CFLAGS='-lquadmath' PIP_ONLY_BINARY=scipy" - # Homebrew's gfortran on the macos-13/14 runners targets the runner's + # Homebrew's gfortran on the macos-14 runner targets the runner's # macOS version, so the libgfortran/libquadmath that delocate bundles # require that minimum. Match MACOSX_DEPLOYMENT_TARGET to the runner or # delocate rejects the wheel ("Library dependencies do not satisfy From 538195201e958d4bb355fe8631287d79813bb99d Mon Sep 17 00:00:00 2001 From: Ben Mather Date: Thu, 23 Jul 2026 14:09:58 +1000 Subject: [PATCH 9/9] pypi: build Linux wheels on manylinux_2_28 (modern GCC + numpy wheels) The default manylinux2014 image ships GCC 10.2. Recent NumPy (2.5+) only publishes manylinux_2_28 wheels, so the PEP 517 build-isolation install of `numpy>=2.0` fell back to a source build for cp312/cp313 and failed: "NumPy requires GCC >= 10.3". Switch the Linux build images to manylinux_2_28 / musllinux_1_2 (GCC 12), where numpy/scipy wheels are available and any source build has a new enough compiler. This raises the Linux wheels' glibc floor to 2.28 (RHEL8/Ubuntu 18.10+), the current manylinux baseline. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01FFBEqxMhk9Ut6vVm5jfV8d --- .github/workflows/pypi.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/pypi.yml b/.github/workflows/pypi.yml index c2bc4682..f539e11f 100644 --- a/.github/workflows/pypi.yml +++ b/.github/workflows/pypi.yml @@ -55,6 +55,13 @@ jobs: CIBW_ENVIRONMENT_MACOS: "MACOSX_DEPLOYMENT_TARGET=${{ matrix.macos_target }} PIP_ONLY_BINARY=scipy" CIBW_PROJECT_REQUIRES_PYTHON: ">=3.9" CIBW_ARCHS: 'auto64' + # Use the modern manylinux/musllinux images (GCC 12). The default + # manylinux2014 image ships GCC 10.2, but recent NumPy only publishes + # manylinux_2_28 wheels and its source build requires GCC >= 10.3, so + # the build-isolation NumPy install would fail there. manylinux_2_28 + # has wheels available and a new enough compiler. + CIBW_MANYLINUX_X86_64_IMAGE: manylinux_2_28 + CIBW_MUSLLINUX_X86_64_IMAGE: musllinux_1_2 CIBW_BEFORE_BUILD_WINDOWS: "choco install ninja" CIBW_BEFORE_BUILD_MACOS: "brew install ninja gcc && brew reinstall gcc" # CIBW_BEFORE_BUILD_LINUX: "apt-get install -y ninja-build"