Integrating CMake with scikit-build-core

The migration from legacy setuptools-driven workflows to PEP 517/518 compliant build backends has standardized how geospatial Python packages are compiled, distributed, and maintained. Within the broader Modern Python Build Tooling & Wheel Configuration ecosystem, integrating CMake with scikit-build-core establishes the definitive pipeline for bridging mature C/C++ spatial libraries (GDAL, PROJ, GEOS) with modern Python wheel distribution. This cluster isolates the CMake-to-Python compilation pipeline: deterministic pyproject.toml declarations, production-grade CMakeLists.txt architecture, cross-platform CI matrix optimization, and ABI-stable wheel generation. Environment provisioning, container base image construction, and registry publishing are intentionally scoped to adjacent documentation.

The backend mediates between the Python build frontend and the native toolchain:

sequenceDiagram participant F as Build frontend participant S as scikit-build-core participant C as CMake participant Z as Compiler F->>S: build_wheel per PEP 517 S->>C: configure and map pyproject to cache vars C->>Z: compile extension Z-->>C: object files and .so C-->>S: staged build tree S-->>F: wheel with correct tags

Core Build Declaration & pyproject.toml Configuration

scikit-build-core operates as a PEP 517 build backend that translates Python packaging metadata into deterministic CMake invocations. The configuration must explicitly declare the backend, enforce minimum tool versions, and define wheel-specific constraints to prevent silent fallbacks or ABI mismatches.

[build-system]
requires = ["scikit-build-core>=0.9.0", "cmake>=3.26", "ninja>=1.11"]
build-backend = "scikit_build_core.build"

[project]
name = "geospatial-ext"
version = "1.2.0"
requires-python = ">=3.9"
dependencies = ["numpy>=1.24"]

[tool.scikit-build]
cmake.version = ">=3.26"
ninja.make-fallback = false
wheel.expand-macos-universal-tags = true
sdist.exclude = [".github", "tests", "docs"]

The [tool.scikit-build] table directly maps Python metadata to CMake cache variables. Setting ninja.make-fallback = false is critical for CI reproducibility; it forces a hard failure if Ninja is missing rather than silently degrading to GNU Make, which breaks parallel build guarantees and invalidates cache consistency. For maintainers requiring granular control over build metadata, wheel tags, and source distribution pruning, the schema extensions documented in Mastering pyproject.toml for Spatial Wheels provide the necessary configuration patterns and validation rules.

CMakeLists.txt Architecture for Geospatial Extensions

Geospatial Python extensions demand strict ABI alignment, explicit symbol visibility, and deterministic library linkage. A production-ready CMakeLists.txt must leverage CMake’s native Python discovery modules rather than legacy FindPythonLibs or distutils hacks.

cmake_minimum_required(VERSION 3.26)
project(geospatial_ext LANGUAGES CXX)

# Modern CMake Python discovery (scikit-build-core supplies the hints)
find_package(Python 3.9 COMPONENTS Interpreter Development.Module REQUIRED)

# Geospatial dependency resolution
find_package(GDAL 3.4 REQUIRED)
find_package(PROJ 9.0 REQUIRED)

# Extension module target. python_add_library(... WITH_SOABI) comes from
# CMake's FindPython: it applies the correct .cpython-*.so suffix and links
# Python::Module automatically.
python_add_library(geospatial_ext MODULE WITH_SOABI src/bindings.cpp)
target_link_libraries(geospatial_ext PRIVATE GDAL::GDAL PROJ::proj)
target_compile_features(geospatial_ext PRIVATE cxx_std_17)

# ABI & Visibility Controls
set_target_properties(geospatial_ext PROPERTIES
    CXX_VISIBILITY_PRESET hidden
    VISIBILITY_INLINES_HIDDEN ON
)

# Install into the wheel (scikit-build-core handles RPATH/repair downstream)
install(TARGETS geospatial_ext LIBRARY DESTINATION .)

Key architectural decisions:

  • Python::Module ensures correct linkage against the Python C-API without polluting the global namespace or triggering duplicate symbol errors.
  • CXX_VISIBILITY_PRESET hidden prevents symbol collisions in complex geospatial stacks where multiple C++ runtimes or conflicting library versions may coexist in downstream environments.
  • python_add_library(... MODULE WITH_SOABI) (from CMake’s FindPython) applies the correct platform-specific extension suffix and links Python::Module. The classic scikit-build python_extension_module() macro is not shipped with scikit-build-core, so it must not be used here.

When resolving complex dependency trees, particularly with PROJ’s CMake config files, maintainers often encounter find_package resolution failures due to missing CMAKE_PREFIX_PATH hints or pkg-config fallbacks. The resolution strategies for these edge cases are detailed in Fixing CMake find_package for PROJ. Similarly, GDAL’s modular architecture requires careful component selection to minimize wheel bloat and avoid unnecessary transitive dependencies, as covered in Optimizing scikit-build-core for GDAL.

Cross-Platform CI & Deterministic Build Execution

Reproducible geospatial wheels require a tightly controlled CI matrix. The build backend natively supports parallel execution via Ninja, but CI runners must be configured to expose the correct compiler toolchains and cache directories.

# GitHub Actions matrix example (structural)
strategy:
  matrix:
    os: [ubuntu-latest, macos-latest, windows-latest]
    python-version: ["3.9", "3.10", "3.11"]
steps:
  - uses: actions/setup-python@v5
  - run: pip install build
  - run: python -m build --wheel

For deterministic builds across runners, isolate the build environment using declarative dependency managers. While scikit-build-core handles the compilation step, the surrounding toolchain (compilers, CMake, Ninja) should be version-pinned and cached. Workflows that integrate Environment Isolation with Pixi and Conda ensure that compiler flags, sysroot paths, and CMake cache states remain consistent across macOS ARM64, Linux x86_64, and Windows MSVC environments. This isolation prevents host-system pollution and guarantees that scikit-build-core receives identical inputs regardless of runner drift.

ABI Stability & Wheel Packaging Validation

Python’s stable ABI (abi3) is generally unsuitable for geospatial extensions due to heavy reliance on the NumPy C-API, pybind11, and third-party C++ libraries that change across minor Python versions. Instead, target explicit cpXY tags and enforce strict C++ standard library linkage.

On Linux, scikit-build-core produces wheels with embedded RPATHs pointing to bundled .so files. Post-build validation with auditwheel is mandatory to strip external dependencies, verify glibc compatibility, and bundle them into the wheel. On macOS, wheel.expand-macos-universal-tags = true enables automatic universal2 slicing, but requires delocate to copy all linked dynamic libraries into the wheel’s .dylibs/ directory and rewrite their install names.

The official CMake FindPython documentation outlines the exact module discovery semantics used by the backend, while the PEP 517 specification defines the isolation guarantees that prevent host-system pollution during wheel generation. For comprehensive backend configuration reference, consult the scikit-build-core documentation.

Integrating CMake with scikit-build-core transforms geospatial Python packaging from a fragile, environment-dependent process into a deterministic, CI-optimized pipeline. By enforcing strict version constraints, modern CMake discovery patterns, and explicit ABI controls, maintainers can produce spatial wheels that are reproducible, secure, and compliant with modern Python packaging standards.