Fixing CMake find_package for PROJ
When building spatial Python extensions, find_package(PROJ) resolution failures are among the most persistent CI/CD blockers. Modern wheel pipelines operating under Modern Python Build Tooling & Wheel Configuration demand deterministic dependency resolution, yet PROJ’s CMake config exports frequently fracture under environment isolation, cross-compilation sysroots, or strict Conda/Pixi activation. This guide provides exact error-to-fix mappings, hardened pyproject.toml configurations, and CI-ready validation steps to restore deterministic builds across Linux, macOS, and Windows runners.
Exact Error Signatures & Root Cause Mapping
CMake emits distinct failure signatures depending on how the build environment leaks or strips PROJ paths. Match the exact log output to the corresponding remediation before modifying build configuration.
| Error Signature | Root Cause | Immediate Fix |
|---|---|---|
CMake Error: Could not find a package configuration file provided by "PROJ" |
PROJ_DIR/CMAKE_PREFIX_PATH not propagated into the isolated CMake generator context |
Explicitly pass prefix paths via cmake.define or CMAKE_ARGS in pyproject.toml |
Could NOT find PROJ (missing: PROJ_LIBRARY PROJ_INCLUDE_DIR) |
Fallback to legacy FindPROJ.cmake module mode; pkg-config unavailable or sysroot mismatched |
Force CONFIG mode resolution and verify PROJConfig.cmake exists in $PREFIX/lib/cmake/proj |
Found PROJ 9.1.0 but required 9.2.0 |
Version constraint mismatch in find_package(PROJ X.Y REQUIRED) or stale cached CMakeCache.txt |
Update find_package version floor, clear build cache, and enforce PROJ_VERSION check |
PROJ_DIR-NOTFOUND |
Environment variable unset, stripped by CI sandbox, or overwritten by scikit-build-core isolation |
Declare PROJ_DIR as a build-system dependency hint and map to cmake.define |
The core failure vector is path leakage. scikit-build-core executes CMake in a hermetic build directory, stripping host environment variables unless explicitly declared. PROJ’s modern CMake exports require precise prefix resolution, which breaks under manylinux cross-compilation or strict Conda environment activation.
Environment Diagnostics & Prefix Resolution
Before modifying pyproject.toml, validate that the runner exposes PROJ correctly. Run these diagnostics directly in the CI step or local shell:
# 1. Verify PROJ binary and pkg-config metadata
pkg-config --modversion proj 2>/dev/null || echo "pkg-config missing or PROJ not registered"
# 2. Locate CMake config exports
find /opt /usr/local "$CONDA_PREFIX" "$PIXI_PROJECT_ENV" -name "PROJConfig.cmake" 2>/dev/null | head -n 3
# 3. Resolve deterministic prefix for CMake
export PROJ_DIR=$(pkg-config --variable=prefix proj 2>/dev/null || echo "/usr/local")
export CMAKE_PREFIX_PATH="${PROJ_DIR}:${CMAKE_PREFIX_PATH}"
On manylinux/manyarm Docker images, PROJ is typically staged under /usr/local or /opt/_internal/cpython-*/lib. When using Pixi or Conda, activate the environment explicitly before invoking the build step, or pass the resolved prefix directly to the generator:
# Conda/Pixi explicit prefix resolution
export PROJ_DIR="${CONDA_PREFIX:-${PIXI_PROJECT_ENV:-/usr/local}}"
# Verify CMake can see it
cmake -DPROJ_DIR="$PROJ_DIR" --trace-expand -P /dev/null 2>&1 | grep -i proj
Refer to the official PROJ CMake integration reference for version-specific export behavior.
Hardened pyproject.toml Configuration
Modern scikit-build-core requires explicit CMake cache variable mapping to survive isolated build environments. Configure [tool.scikit-build] to forward PROJ paths, enforce CONFIG mode, and enable pkg-config fallbacks. This aligns with the architecture detailed in Integrating CMake with scikit-build-core.
[build-system]
requires = ["scikit-build-core>=0.9.0", "pybind11>=2.10"]
build-backend = "scikit_build_core.build"
[project]
name = "spatial-extension"
version = "1.0.0"
requires-python = ">=3.9"
[tool.scikit-build]
cmake.version = ">=3.25"
cmake.args = ["-DCMAKE_FIND_PACKAGE_PREFER_CONFIG=ON"]
wheel.py-api = "cp39"
wheel.packages = ["src/spatial_extension"]
# CMake cache variables — define the table once (declaring cmake.define both as
# an inline table and as a [section] is invalid TOML). Override per-runner via:
# pip wheel . --config-settings=cmake.define.PROJ_DIR=/opt/proj
[tool.scikit-build.cmake.define]
PROJ_DIR = "/usr/local"
PROJ_USE_STATIC_LIBS = "OFF"
CMAKE_FIND_DEBUG_MODE = "OFF"
Key configuration notes:
cmake.version = ">=3.25"ensures modernFindPackagebehavior andCONFIGmode prioritization.CMAKE_FIND_PACKAGE_PREFER_CONFIG=ONmakes CMake try CONFIG mode (PROJConfig.cmake) before the legacyFindPROJ.cmakemodule; to require config mode outright, callfind_package(PROJ CONFIG REQUIRED)inCMakeLists.txt.PROJ_DIRdefaults to/usr/localand can be overridden per-runner with--config-settings=cmake.define.PROJ_DIR=/path(scikit-build-core also forwardsCMAKE_PREFIX_PATHfrom the environment), guaranteeing deterministic resolution when CI strips variables.- Avoid hardcoding absolute paths in
CMakeLists.txt. Always consume${PROJ_DIR}or${CMAKE_PREFIX_PATH}injected by the build backend.
Cross-Platform CI/CD Validation
After applying the configuration patch, validate linkage and wheel compliance before merging.
Linux (manylinux/manyarm)
# Build wheel
pip wheel . --no-build-isolation -w dist/
# Verify dynamic linkage against PROJ
ldd dist/spatial_extension*.so | grep -i proj
# Enforce manylinux compliance
auditwheel repair dist/*.whl --plat manylinux_2_34_x86_64 -w repaired/
macOS
# Build wheel
pip wheel . --no-build-isolation -w dist/
# Verify dylib linkage and rpath resolution
otool -L dist/spatial_extension*.so | grep -i proj
# Delocate to bundle PROJ if required
delocate-wheel -w repaired/ dist/*.whl -v
Windows
# Build wheel
pip wheel . --no-build-isolation -w dist/
# Verify DLL resolution
dumpbin /DEPENDENTS dist\spatial_extension*.pyd | findstr /i proj
CI Pipeline Integration
Embed these checks in your GitHub Actions or GitLab CI matrix to catch regressions early:
- name: Validate PROJ linkage
run: |
pip wheel . --no-build-isolation -w dist/
pip install --no-index --find-links dist/ spatial-extension
python -c "import spatial_extension; print('Import successful')"
python -m pip install auditwheel
auditwheel show dist/*.whl
Compliance & Deterministic Build Standards
Spatial Python wheels must adhere to PyPA packaging standards and geospatial ecosystem conventions:
- PEP 517/518 Compliance: Declare
scikit-build-corein[build-system].requires. Never rely on implicit host CMake installations. - Deterministic Builds: Disable
CMAKE_FIND_DEBUG_MODEin production. Use--config-settings=cmake.define.PROJ_DIR=/pathto override environment variables reproducibly. - Wheel Tagging: Align
wheel.py-apiwith your supported Python minor versions. Useauditwheel/delocateto bundle PROJ only when targetingmanylinux/macOS wheels that lack system PROJ. - Version Pinning: Enforce
find_package(PROJ 9.2 REQUIRED)inCMakeLists.txtto prevent ABI drift. PROJ’s C API is stable, but internal symbol visibility changes between major releases. - Build Isolation: Respect
--no-build-isolationonly for local debugging. CI runners should use isolated environments to guarantee reproduciblePROJConfig.cmakeresolution.
For authoritative guidance on declaring build dependencies and backend isolation, consult the PyPA build backend specifications.
By enforcing explicit prefix mapping, disabling legacy module fallbacks, and validating linkage post-build, you eliminate find_package(PROJ) flakiness and align spatial extension pipelines with enterprise-grade reproducibility standards.