Configuring pixi environments for wheel building
Geospatial Python wheels fail in CI/CD pipelines due to three deterministic root causes: environment drift across runners, missing or misrouted C/C++ toolchains, and implicit host library resolution during CMake configuration. When wrapping GDAL, PROJ, or PyProj, relying on system packages or unpinned conda channels guarantees non-reproducible builds. Configuring pixi for wheel generation requires strict dependency pinning, explicit compiler routing, and deterministic artifact generation. This guide provides production-ready templates, exact error signatures, and recovery workflows aligned with PyPA standards and the broader Modern Python Build Tooling & Wheel Configuration ecosystem.
Production-Ready pixi.toml Baseline
The following configuration isolates the build environment, pins geospatial C-libs for ABI stability, and routes scikit-build-core through Pixi’s native task runner. It enforces platform-specific compiler injection and disables pip’s binary fallback to guarantee source compilation.
[project]
name = "geospatial-wheel-builder"
version = "0.1.0"
description = "Isolated build environment for GDAL/PROJ/PyProj spatial wheels"
channels = ["conda-forge"]
platforms = ["linux-64", "osx-arm64", "win-64"]
[dependencies]
python = ">=3.10,<3.13"
build = ">=1.2.0"
scikit-build-core = ">=0.10.0"
cmake = ">=3.28"
ninja = ">=1.11"
# Geospatial C-libraries (pinned for ABI stability)
gdal = ">=3.8,<3.9"
proj = ">=9.3,<9.4"
libgdal = ">=3.8,<3.9"
# Platform-specific compilers (conda-forge naming convention)
[target.linux-64.dependencies]
gcc_linux-64 = ">=13.0"
[target.osx-arm64.dependencies]
clang_osx-arm64 = ">=16.0"
[target.win-64.dependencies]
vs2022_win-64 = ">=19.38"
[pypi-dependencies]
pyproj = ">=3.6.0"
rasterio = ">=1.3.0"
[activation.env]
GDAL_DATA = "$PIXI_ENV_PREFIX/share/gdal"
PROJ_LIB = "$PIXI_ENV_PREFIX/share/proj"
PROJ_NETWORK = "OFF"
CMAKE_GENERATOR = "Ninja"
# Force source compilation during wheel build
PIP_NO_BINARY = ":all:"
[tasks]
build-wheel = "python -m build --wheel --no-isolation"
validate = "python -c \"import pyproj, rasterio; print('Import validation passed')\""
clean = "rm -rf dist/ build/ *.egg-info"
Key DevOps Rationale:
--no-isolationpreventsbuildfrom spawning a temporary venv, ensuring Pixi’s resolved toolchain is used.PIP_NO_BINARY = ":all:"blocks pip from downloading incompatible precompiled wheels during dependency resolution in the build step.- Platform-targeted compiler blocks leverage conda-forge’s cross-compilation packages, which Pixi resolves automatically based on the runner architecture. For deeper isolation patterns, see Environment Isolation with Pixi and Conda.
Compiler Routing and CMake Isolation
Geospatial packages rely on scikit-build-core to bridge Python packaging with native CMake toolchains. While Pixi injects compiler binaries into $PATH, CMake’s find_package() will silently resolve to host-installed libraries if roots are not explicitly scoped. This causes RPATH corruption and runtime ImportError failures on deployment targets.
Add a CMakePresets.json at the repository root to enforce isolated toolchain resolution:
{
"version": 3,
"configurePresets": [
{
"name": "pixi-geospatial",
"generator": "Ninja",
"cacheVariables": {
"GDAL_ROOT": "$env{PIXI_ENV_PREFIX}",
"PROJ_ROOT": "$env{PIXI_ENV_PREFIX}",
"CMAKE_PREFIX_PATH": "$env{PIXI_ENV_PREFIX}",
"CMAKE_INSTALL_RPATH_USE_LINK_PATH": "ON",
"CMAKE_BUILD_TYPE": "Release"
}
}
]
}
Execution Flow:
- Run
pixi run build-wheel. Pixi materializes the activation environment before invokingpython -m build. scikit-build-coredetectsCMakePresets.jsonand applies the cache variables during the configure phase.CMAKE_INSTALL_RPATH_USE_LINK_PATHadds the Pixi prefix’s link directories to the build-tree RPATH so the extension can loadlibgdal.so/libproj.soduring local testing. Because those are absolute paths, a distributable wheel still needs a post-buildauditwheel repair(Linux) ordelocate(macOS) pass to rewrite them to relative$ORIGIN-based paths.
For detailed CMake backend configuration, reference the official scikit-build-core documentation.
Exact Error-to-Fix Mapping for Spatial Wheels
The following table maps deterministic CI/CD failure signatures to precise remediation steps. Use this for pipeline triage and runbook automation.
| CI/CD Error Signature | Root Cause | Exact Fix |
|---|---|---|
CMake Error: Could not find a package configuration file provided by "GDAL" |
CMake searches system /usr/lib instead of Pixi env. |
Add CMAKE_PREFIX_PATH: "$env{PIXI_ENV_PREFIX}" to CMakePresets.json. Verify GDAL_ROOT points to $PIXI_ENV_PREFIX. |
ImportError: libproj.so.25: cannot open shared object file: No such file or directory |
Missing RPATH in compiled extension. |
Set CMAKE_INSTALL_RPATH_USE_LINK_PATH=ON in presets. Validate with readelf -d dist/*.so | grep RPATH. |
pip install pulls prebuilt wheels incompatible with pinned C-libs |
Pip binary fallback overrides source build. | Enforce PIP_NO_BINARY = ":all:" in [activation.env]. Add --no-binary :all: to build-wheel task if needed. |
scikit-build-core: Ninja not found |
ninja missing from environment or $PATH not refreshed. |
Pin ninja = ">=1.11" in [dependencies]. Run pixi install before build. Set CMAKE_GENERATOR = "Ninja". |
ModuleNotFoundError: No module named 'pyproj._network' |
pyproj built against mismatched PROJ ABI. |
Pin proj and libproj to identical minor versions. Rebuild with pixi run clean && pixi run build-wheel. |
auditwheel repair fails with cannot find libgdal.so.34 |
Manylinux policy mismatch or missing LD_LIBRARY_PATH during repair. |
Use manylinux_2_28_x86_64 base for GDAL 3.8+. Pass --wheel-dir dist/ to auditwheel with LD_LIBRARY_PATH=$PIXI_ENV_PREFIX/lib. |
Deterministic Validation and PyPA Compliance
Post-build validation must verify wheel structure, ABI tags, and runtime linkage before artifact publication. Follow this sequence to enforce PyPA compliance:
- Inspect Wheel Metadata:
python -c "import zipfile; z=zipfile.ZipFile('dist/geospatial_wheel_builder-0.1.0-cp311-cp311-manylinux_2_28_x86_64.whl'); print('\n'.join(z.namelist()))"
Verify .dist-info/WHEEL contains Generator: scikit-build-core and Root-Is-Purelib: false.
-
Validate ABI Tags: Ensure the wheel filename matches PEP 427 conventions. For Linux,
manylinux_2_28is required for GDAL 3.8+. Useauditwheel show dist/*.whlto confirm external library linkage. -
Runtime Import Check:
pixi run validate
Execute in a clean runner environment to confirm dynamic linker resolution succeeds without LD_LIBRARY_PATH overrides.
- Dependency Graph Verification:
pip check
Confirms no version conflicts between pyproj, rasterio, and the compiled extensions.
CI/CD Pipeline Integration Patterns
Deploy this configuration across GitHub Actions, GitLab CI, or Jenkins using matrix builds. Cache the .pixi directory to reduce environment resolution time from ~45s to ~8s.
# GitHub Actions snippet
- uses: prefix-dev/setup-pixi@v0.8.0
with:
pixi-version: v0.30.0
cache: true
cache-write: ${{ github.ref == 'refs/heads/main' }}
- run: pixi run build-wheel
- uses: actions/upload-artifact@v4
with:
name: spatial-wheels-${{ matrix.platform }}
path: dist/*.whl
retention-days: 5
Cache Strategy Notes:
- Pixi caches resolved environments at
~/.cache/pixi. Enablecache-writeonly onmainto prevent cache poisoning from feature branches. - For cross-platform compilation, pair this setup with Async Build Execution and Cache Strategies to parallelize wheel generation across
linux-64,osx-arm64, andwin-64runners. - Always run
pixi run cleanbeforebuild-wheelin CI to prevent stale.pycor CMake cache artifacts from leaking into the build context.
By enforcing strict environment pinning, explicit CMake routing, and automated linkage validation, geospatial maintainers eliminate the majority of spatial wheel build failures. This configuration aligns with modern Python packaging standards and scales deterministically across enterprise CI/CD registries.