Skip to content

Commit 4211688

Browse files
Technologicatclaude
andcommitted
CI: green test matrix, three cleanups after the MSVC fix
Previous run confirmed the MSVC-on-Windows fix works — all 12 test matrix jobs (ubuntu/macos/windows × py3.11/3.12/3.13/3.14) are green. sdist is green too. Only build-wheels failed, and only for one real reason (macOS). Three cleanups in this commit: 1. Remove the temporary Windows DLL diagnostic step from the test job. It lived at .github/workflows/ci.yml as `Diagnose extension DLL dependencies (Windows)`. Its purpose was to identify the MinGW-vs- MSVC issue, which it did, and now that the ilammy/msvc-dev-cmd step is landed the diagnostic is pure CI log noise. The full diagnostic recipe (pefile import table + ctypes.WinDLL probe + spec-load PyInit probe) is preserved in ~/.claude/CI-SETUP-NOTES.md under "Windows CI for Cython extensions: force MSVC" for future reuse. 2. Add `fail-fast: false` to the build-wheels matrix. Without it, a single-OS wheel failure cancels the other OSes mid-build, so the previous run showed "all three build-wheels jobs failed" when in reality only macOS actually failed — linux and windows were just cascade-canceled while pulling the manylinux image / setting up MSVC. The test matrix already has fail-fast: false; this aligns build-wheels with the same policy. 3. Drop `brew install libomp` from the cibuildwheel macOS before-all. The macOS wheel job actually failed because delocate-wheel refused to bundle Homebrew's libomp 22.1.2: that libomp.dylib has a minimum macOS target of 15.0, but cibuildwheel's default arm64 target is 11.0, so bundling it would produce a wheel that lies about its compatibility. Options considered: (a) Bump MACOSX_DEPLOYMENT_TARGET to 15.0 — shrinks supported macOS range by ~4 years. (b) Build libomp from source with a lower target in before-all — complex CI, fragile. (c) Drop libomp entirely, let meson's `dependency('openmp', required: false)` fall back to serial. We take (c). Cython's `prange` silently degrades to `range` when OpenMP is not present, so the tests still pass and every numerical invariant still holds — the macOS wheel is just single-threaded. Users who want parallel fits on macOS can install from source after `brew install libomp` (README will document this in Phase 8). Linux and Windows wheel builds are unaffected by this change — Linux has GCC + libgomp in the manylinux container, Windows has MSVC's vcomp140 activated via ilammy/msvc-dev-cmd. Both continue to produce parallel wheels. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 9df432b commit 4211688

File tree

2 files changed

+17
-108
lines changed

2 files changed

+17
-108
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -81,119 +81,14 @@ jobs:
8181
- name: Install wlsqm
8282
run: pip install --no-build-isolation -e .
8383

84-
# Diagnose Windows DLL-load failures: first run said vcomp140 is
85-
# resolvable via ctypes, so the problem is deeper in the chain. Use
86-
# pefile to dump the DLL-level import table of every built .pyd, then
87-
# probe each .pyd with ctypes.WinDLL (which only exercises the Windows
88-
# loader — PyInit never runs, so a WinDLL failure means Windows could
89-
# not resolve a direct DLL import, whereas a WinDLL success + Python
90-
# import failure means the Cython cimport chain in PyInit is what
91-
# fails).
92-
- name: Diagnose extension DLL dependencies (Windows)
93-
if: runner.os == 'Windows'
94-
# Use Git Bash so the Python heredoc works. PowerShell does not
95-
# understand `python - <<'PY' ... PY` and chokes at parse time.
96-
shell: bash
97-
run: |
98-
python -m pip install -q pefile
99-
python - <<'PY'
100-
import ctypes, glob, importlib.util, os, sys, traceback
101-
import pefile
102-
103-
print('python:', sys.version)
104-
print('prefix:', sys.prefix)
105-
106-
# meson-python editable install drops the compiled .pyd files
107-
# under build/<tag>/wlsqm/..., not under the source tree. The
108-
# editable loader redirects imports there, so that is where we
109-
# have to look.
110-
pyds = sorted(glob.glob('build/**/*.pyd', recursive=True))
111-
print('=== built .pyd files ===')
112-
for p in pyds:
113-
print(' ', p)
114-
115-
print()
116-
print('=== DLL-level imports per .pyd (pefile) ===')
117-
for p in pyds:
118-
try:
119-
pe = pefile.PE(p, fast_load=True)
120-
pe.parse_data_directories(directories=[pefile.DIRECTORY_ENTRY['IMAGE_DIRECTORY_ENTRY_IMPORT']])
121-
imports = [e.dll.decode(errors='replace') for e in getattr(pe, 'DIRECTORY_ENTRY_IMPORT', [])]
122-
print(f' {p} ->')
123-
for name in imports:
124-
print(f' {name}')
125-
pe.close()
126-
except Exception as e:
127-
print(f' {p} -> pefile error: {e}')
128-
129-
print()
130-
print('=== ctypes.WinDLL probe per .pyd (DLL-level LoadLibrary only, no PyInit) ===')
131-
for p in pyds:
132-
try:
133-
ctypes.WinDLL(os.path.abspath(p))
134-
print(f' WinDLL OK : {p}')
135-
except OSError as e:
136-
print(f' WinDLL FAIL: {p} -- {e}')
137-
138-
print()
139-
print('=== importlib spec-load probe (runs PyInit for each .pyd individually) ===')
140-
# spec_from_file_location loads a .pyd by filesystem path and
141-
# runs its PyInit without going through wlsqm/__init__.py. This
142-
# isolates which module's PyInit is the first to raise.
143-
#
144-
# Extensions that `cimport` other wlsqm modules will still
145-
# trigger Python-level imports during PyInit, and those
146-
# DO go through the package machinery — but we load in
147-
# dependency order below, so by the time simple.pyx's PyInit
148-
# runs, its cimported deps are already in sys.modules.
149-
load_order = [
150-
('wlsqm.fitter.defs', 'fitter/defs'),
151-
('wlsqm.fitter.infra', 'fitter/infra'),
152-
('wlsqm.fitter.polyeval', 'fitter/polyeval'),
153-
('wlsqm.utils.ptrwrap', 'utils/ptrwrap'),
154-
('wlsqm.utils.lapackdrivers', 'utils/lapackdrivers'),
155-
('wlsqm.fitter.interp', 'fitter/interp'),
156-
('wlsqm.fitter.impl', 'fitter/impl'),
157-
('wlsqm.fitter.simple', 'fitter/simple'),
158-
('wlsqm.fitter.expert', 'fitter/expert'),
159-
]
160-
def find_pyd(subpath):
161-
# subpath is like 'fitter/defs' or 'utils/lapackdrivers'.
162-
# Match by subdir + filename stem; the ABI tag varies with
163-
# the Python version in the test matrix, so we don't hardcode it.
164-
subdir, stem = subpath.split('/')
165-
for p in pyds:
166-
norm = p.replace('\\', '/')
167-
if f'/{subdir}/' in norm and os.path.basename(norm).split('.')[0] == stem:
168-
return p
169-
return None
170-
171-
for fqname, subpath in load_order:
172-
p = find_pyd(subpath)
173-
if p is None:
174-
print(f' not found : {fqname}')
175-
continue
176-
try:
177-
spec = importlib.util.spec_from_file_location(fqname, p)
178-
mod = importlib.util.module_from_spec(spec)
179-
sys.modules[fqname] = mod
180-
spec.loader.exec_module(mod)
181-
print(f' PyInit OK : {fqname} ({p})')
182-
except Exception as e:
183-
print(f' PyInit FAIL: {fqname} ({p})')
184-
print(f' {type(e).__name__}: {e}')
185-
traceback.print_exc()
186-
break
187-
PY
188-
continue-on-error: true
189-
19084
- name: Run tests
19185
run: pytest tests/ -v
19286

19387
build-wheels:
19488
needs: test
19589
runs-on: ${{ matrix.os }}
19690
strategy:
91+
fail-fast: false
19792
matrix:
19893
os: [ubuntu-latest, macos-latest, windows-latest]
19994
steps:

pyproject.toml

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,8 +119,22 @@ skip = "*-win32 *-manylinux_i686 *-musllinux*"
119119
test-requires = ["pytest", "numpy", "scipy"]
120120
test-command = "pytest {project}/tests -v"
121121

122-
[tool.cibuildwheel.macos]
123-
before-all = "brew install libomp"
122+
# macOS wheels ship without OpenMP. We could `brew install libomp` in a
123+
# `[tool.cibuildwheel.macos] before-all` step and let delocate-wheel bundle
124+
# libomp.dylib into the wheel, but Homebrew's current libomp (22.x) has a
125+
# minimum macOS target (15.0) newer than cibuildwheel's default arm64 target
126+
# (11.0). delocate-wheel correctly refuses to bundle it because the resulting
127+
# wheel would lie about its compatibility.
128+
#
129+
# We could either (a) bump MACOSX_DEPLOYMENT_TARGET to 15.0, shrinking the
130+
# supported macOS range by ~4 years, or (b) build libomp from source with a
131+
# lower target (complex CI). Instead, we lean on meson's
132+
# `dependency('openmp', required: false)` fallback: without libomp on the
133+
# build host, meson builds the extension in serial mode. Cython's `prange`
134+
# degrades to plain `range`, tests still pass, and macOS wheels are slower
135+
# but portable all the way back to macOS 11. Users who want parallel fits
136+
# on macOS can install from source: `brew install libomp` and then
137+
# `pip install --no-binary wlsqm wlsqm`. README.md documents this.
124138

125139
[dependency-groups]
126140
dev = [

0 commit comments

Comments
 (0)