Skip to content

ci: refresh Python matrix and ASE compat for meson#27

Merged
jameskermode merged 16 commits into
mesonfrom
ci/python-version-update-meson
Apr 28, 2026
Merged

ci: refresh Python matrix and ASE compat for meson#27
jameskermode merged 16 commits into
mesonfrom
ci/python-version-update-meson

Conversation

@jameskermode
Copy link
Copy Markdown
Member

Summary

  • Drop EOL Python from both the test matrix and the cibuildwheel matrix (3.8 EOL Oct 2024; 3.9 EOL Oct 2025) and add 3.13. Removes the now-unneeded macOS-arm64/3.8 `exclude` entry.
  • Add a try/except import shim for `full_3x3_to_voigt_6_stress` (relocated to `ase.stress` in ASE 3.28+) in `python/extxyz/utils.py` and `tests/test_ase_cases.py`. Same fix as Update utils.py #25.

Verified locally on macOS arm64 with Python 3.13.13 + ASE 3.28.0: `pytest tests/` → 31 passed, 2 skipped.

Note: this PR targets the long-running `meson` migration branch (#17), not master.

Test plan

  • CI Python 3.10 build green
  • CI Python 3.11 build green
  • CI Python 3.12 build green
  • CI Python 3.13 build green
  • cibuildwheel matrix builds wheels for the new Python versions on Linux + macOS

🤖 Generated with Claude Code

jameskermode and others added 16 commits April 27, 2026 20:59
Python 3.8 (EOL Oct 2024) and 3.9 (EOL Oct 2025) are no longer
supported upstream. Replace them with 3.13 in both the test
matrix and the cibuildwheel matrix. The Python 3.8 macOS-arm64
exclusion is dropped along with its trigger.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ase.constraints.full_3x3_to_voigt_6_stress was relocated to
ase.stress in newer ASE releases, hard-failing the import on
Python 3.13 + ASE 3.28. Add a try/except import shim in both
extxyz.utils and the test that imports the helper directly,
matching the fix from PR #25.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Both workflows previously fired only on push/PR against master,
so PRs targeting the long-running meson migration branch never
ran CI. Add meson to the push and pull_request branch filters
so this and future meson-targeted PRs get checked.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Linux: bump CIBW_MANYLINUX_X86_64_IMAGE to manylinux_2_28.
  scipy 1.17 (released since the last green CI in Nov 2025)
  dropped manylinux2014 wheels for cp311+, so the test phase
  fell back to a from-source scipy build that needs OpenBLAS
  in the container.
- macOS Intel (macos-15-intel): bump MACOSX_DEPLOYMENT_TARGET
  from 14.0 to 15.0. Brew on macos-15 runners now ships
  PCRE2 with a 15.0 minimum, so building against it under a
  14.0 target tripped delocate's library-version check.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
meson-python errored on Windows wheel-packaging because Meson
lists an MSVC import library (_extxyz.cpXX-win_amd64.lib) in
its install plan that python.extension_module() never actually
creates. Add tool.meson-python.wheel.exclude = ["**/*.lib"]
to drop those phantom entries from the wheel manifest before
the copy step, then put windows-latest back into the matrix.

If this works, supersedes the long thread of attempts in the
disabled-Windows commit (688316f).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The wheel.exclude fix solved the meson-python install error,
revealing the next-layer Windows issue: cextxyz.py loads the
extension via ctypes.CDLL and calls extxyz_read_ll, etc.
directly, but MSVC defaults to no exported symbols, so
ctypes raised AttributeError on first lookup.

Add libextxyz/_extxyz.def listing the five symbols cextxyz.py
calls, and pass vs_module_defs to python.extension_module().
The vs_module_defs kwarg is a no-op on non-MSVC compilers, so
gnu_symbol_visibility='default' continues to handle GCC/Clang.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
cibuildwheel's default repair-wheel-command on Windows is
empty (unlike auditwheel on Linux and delocate on macOS), so
the built .pyd shipped with no bundled PCRE2 DLL. ctypes.CDLL
then failed at import time with "Could not find module ... or
one of its dependencies".

Wire delvewheel into the Windows pipeline by installing it in
before-build and pointing repair-wheel-command at it. delvewheel
reads PATH (which already includes C:/vcpkg/installed/x64-windows/bin
via [tool.cibuildwheel.windows.environment]) to find pcre2-8.dll.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
On Linux/macOS cextxyz.py loaded libc via find_library('c') and
called fopen/fclose/ftell/fseek directly, then passed the FILE*
to extxyz_read_ll. That works because libc is system-wide and
both ends share one C runtime.

On Windows find_library('c') returns None (TypeError on
ctypes.CDLL(None)), and even if we loaded msvcrt.dll the
FILE* it returns is generally incompatible with the ucrtbase
runtime that _extxyz.pyd is linked against — that's a classic
CRT-mismatch crash waiting to happen.

Fix: add thin extxyz_fopen/fclose/ftell/fseek wrappers in
extxyz.c, export them via _extxyz.def, and have cextxyz.py
pick that path on Windows. The wrappers compile inside _extxyz
itself so they always share its CRT. Linux/macOS retain the
existing libc path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
iread() checked isinstance(file, PosixPath), which is a Linux/
macOS-only subclass. On Windows tmp_path / 'foo.xyz' returns a
WindowsPath, so the check fell through and the path object went
into the index branch where iteration produced
"WindowsPath object is not iterable".

Switch to isinstance(file, (str, Path)) — pathlib.Path is the
abstract base for both PosixPath and WindowsPath. cfopen
already wraps with str(), so the rest of the path is unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
windows-latest defaults to PowerShell, which can't parse
bash's [[ =~ ]] regex syntax — it tripped on the Check tag
step after every Windows wheel job (PowerShell ParserError),
turning otherwise-green test runs red. Pin shell: bash on
both Check tag and Deploy to PyPI; Git for Windows supplies
bash on the hosted runners.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
QUIP retired the example QUIP/.github/workflows/Makefile.inc
that the old build step copied, breaking every test job since.
QUIP now ships first-class Meson support, so build libAtoms
directly with meson setup + meson compile.

Adjust the Fortran-executable step to point QUIP_LDFLAGS and
QUIP_F90FLAGS at Meson's build layout: liblibAtoms.so under
src/libAtoms, .mod files in the target-private *.p dir
alongside it. Use libopenblas (Meson QUIP's BLAS choice)
instead of separate libblas/liblapack.

Drop the QUIP_ARCH and HAVE_GAP env vars that only made sense
under the old Makefile.inc workflow.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
QUIP's src/Potentials/meson.build references the GAP variable
unconditionally, so meson setup -Dgap=false errors with
"Unknown variable 'GAP'" before any target builds. We only
need libAtoms, but pass -Dgap=true to satisfy the configure
step. The recursive clone already pulls the GAP submodule.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
QUIP's Fortran modules carry the _module suffix
(libatoms_module.mod, system_module.mod, ...), so the original
find for 'system.mod' produced no match and gfortran got an
empty -I argument. Switch the probe to libatoms_module.mod and
fail loudly if it's missing instead of silently passing -I
with no path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The find result was relative to PWD, but make runs from
libextxyz/ — gfortran resolved -I against libextxyz/QUIP/...
which doesn't exist. Anchor the find to ${PWD} so the
returned path is absolute.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
QUIP's libAtoms references f90wrap_abort_ which is provided by
the f90wrap_stub static library (f90wrap_stub.F90 lives in a
separate target so it isn't linked into Python wrappers).
'meson compile libAtoms' alone leaves libf90wrap_stub.a
unbuilt, so the standalone Fortran linker errored on
"undefined reference to f90wrap_abort_".

Compile both targets and add -lf90wrap_stub to QUIP_LDFLAGS.
Verified locally on macOS.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
call print(at) is purely diagnostic — it dumps the parsed
Atoms object to stdout and isn't part of the read/write
contract. On current QUIP/libAtoms (Meson HEAD as of April
2026) it segfaults inside the Properties dictionary print
iterator. Comment it out so the round-trip test stays
functional. Re-enable after the libAtoms regression is fixed
upstream or fextxyz is taught the new dictionary layout.

Verified locally on macOS arm64 with USE_FORTRAN=T pytest:
31 passed, 2 skipped.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@jameskermode jameskermode merged commit 2b531a0 into meson Apr 28, 2026
20 checks passed
@jameskermode jameskermode mentioned this pull request Apr 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant