CI Matrix Recipes for Spatial Wheels
A geospatial wheel matrix has more axes than a pure-Python one — interpreter ABI, operating system, CPU architecture, and libc all multiply — and the same conceptual grid must be expressed twice if you support both GitHub Actions and GitLab CI. This guide sits under the Modern Python Build Tooling & Wheel Configuration reference and gives platform teams side-by-side, copy-pasteable matrix definitions for building GDAL/PROJ/GEOS wheels, plus the caching keys and fan-in step that turn a grid of jobs into one publishable set of artifacts. It targets cibuildwheel 3.0+, GitHub Actions and GitLab CI, GDAL 3.8.x, and the abi3 build that collapses the interpreter axis established in C-API vs CPython ABI compatibility.
Prerequisites & Environment
- A wheel that builds under
cibuildwheellocally, configured per configuring cibuildwheel in pyproject.toml for GDAL — the CI file should be a thin driver around it, not a second source of build logic. - Runner access for each target: GitHub-hosted
ubuntu-latest,macos-14,windows-latest; GitLab needs a Docker executor for Linux and, for macOS/Windows, shell runners. - A cache backend: GitHub
actions/cache, GitLabcache:keyed on the native-dependency lock.
# The one command both CI systems ultimately run
pipx run cibuildwheel --output-dir wheelhouse
Core Configuration
The invariant across both systems is: define the OS/arch grid, run the identical cibuildwheel step, key the cache on the native versions, and upload per-job artifacts that a final job collects. Only the YAML dialect differs.
| Concern | GitHub Actions | GitLab CI |
|---|---|---|
| Grid definition | strategy.matrix.include |
parallel:matrix |
| Per-job artifact | actions/upload-artifact |
artifacts:paths |
| Cache | actions/cache keyed on lock hash |
cache:key:files |
| Fan-in | a needs: job with download-artifact |
a needs: job in a later stage |
| aarch64 | QEMU or native ARM runner | Docker --platform or ARM runner |
The two full recipes are GitHub Actions matrix for GDAL wheel builds and GitLab CI pipeline for spatial wheels.
Step-by-Step Implementation
-
Collapse the interpreter axis first. Build one abi3 wheel per platform (
build = "cp39-*"), not one per Python version — this alone cuts the matrix by 4–5×. -
Enumerate only the OS/arch cells you ship, and mark
fail-fast: false(GitHub) orallow_failureselectively so one arch failing does not cancel the rest. -
Key the cache on native versions, not Python. The expensive artifact is compiled GDAL, so the key is
gdal3.8-proj9.3-${hashFiles('native.lock')}— the strategy in async build execution and cache strategies. -
Fan in. A final job gathers every per-job wheel into one directory and runs a single
twine checkbefore publishing.
Verification
# 1. Enumerate what the matrix will build — no surprises
pipx run cibuildwheel --print-build-identifiers
# expected: one cp39-abi3 identifier per OS/arch cell you declared
# 2. After the fan-in job, the collected set covers every platform
ls dist/ | sed -E 's/.*-(manylinux|musllinux|macosx|win).*/\1/' | sort -u
# expected: manylinux, musllinux, macosx, win — every promised platform present
# 3. Metadata is publishable
python -m twine check dist/*
# expected: PASSED for every wheel
Optimization & Edge Cases
- Emulated aarch64 dominates wall-clock. A QEMU
aarch64cell can be 10× the native cells; either use a native ARM runner or cross-compile per building aarch64 GDAL wheels without QEMU. - Prune before you optimize. Drop cells nobody installs (32-bit, PyPy) rather than speeding them up. Read your download stats first.
- Cache write only on the default branch so feature branches cannot poison the shared native-build cache.
Troubleshooting
One arch fails and cancels the whole matrix. fail-fast defaults to true on GitHub; set it false so a musllinux break does not discard finished manylinux wheels.
No space left on device on the runner. GDAL’s native build plus Docker layers fills the default runner disk. Prune Docker between steps or use a larger runner; this bites the emulated Linux cells first.
Artifacts collide on upload. Two jobs uploading wheelhouse/*.whl under the same artifact name overwrite each other. Name artifacts per cell (wheels-${os}-${arch}) and merge in the fan-in job.
Related
- GitHub Actions matrix for GDAL wheel builds — the complete Actions workflow with caching and fan-in.
- GitLab CI pipeline for spatial wheels — the same grid expressed with
parallel:matrixand Docker executors. - Async build execution and cache strategies — the caching keys that keep the matrix from recompiling GDAL every run.
Further Reading
cibuildwheelCI examples (cibuildwheel.readthedocs.io/en/stable/setup/).