How to fix ABI version mismatch in GDAL wheels

When a Python geospatial pipeline fails during wheel installation or runtime import, the root cause is almost always a collision between the compiled C-extension ABI tag and the native GDAL shared library version. Understanding how to fix ABI version mismatch in GDAL wheels requires isolating whether the failure originates from the Python interpreter ABI (cp39, cp310, abi3), the underlying GDAL C-API version (libgdal.so.30 vs .32), or broken RPATH resolution in manylinux artifacts. This guide provides exact diagnostics, pipeline recovery steps, and copy-paste configurations for maintainers and DevOps teams.

Start from the error signature and follow the branch to its fix:

flowchart TD ERR["ImportError or undefined symbol"] --> Q{"Error signature?"} Q -->|"cannot open libgdal.so.NN"| FIX1["RPATH missing: auditwheel repair injects $ORIGIN/.libs"] Q -->|"undefined symbol GDALOpenEx"| FIX2["GDAL C-API mismatch: pin build to runtime, re-vendor"] Q -->|"GLIBC_2.NN not found"| FIX3["Platform tag too new: rebuild on older manylinux"] Q -->|"compiled against GDAL X.Y"| FIX4["ABI guard tripped: align GDAL_VERSION and rebuild"]

Rapid Triage: Exact Error Signatures & Immediate Diagnostics

Identify the mismatch type before modifying build configurations. Run these commands immediately after a failed pip install or import:

# 1. Verify CPython interpreter tag
python -c "import sysconfig; print(sysconfig.get_platform())"

# 2. Inspect shared library linkage for the compiled extension
ldd $(python -c "import osgeo._gdal as _g; print(_g.__file__)") 2>/dev/null | grep -iE "gdal|proj"

# 3. Verify auditwheel RPATH injection and platform tags
auditwheel show dist/*.whl | grep -A2 "WHEEL"

Exact error-to-fix mapping:

Error Signature Root Cause Immediate Fix
ImportError: libgdal.so.32: cannot open shared object file Dynamic linker cannot locate vendored libgdal.so. RPATH is missing or overwritten by LD_LIBRARY_PATH. Run auditwheel repair --plat manylinux_2_28_x86_64 dist/*.whl to inject correct $ORIGIN/.libs paths.
ImportError: ... undefined symbol: GDALOpenEx C-API version mismatch. Wheel compiled against GDAL ≥3.4, runtime resolves older system libgdal.so. Pin build-time GDAL to match deployment runtime, or force vendoring via pip install --no-binary :all: gdal.
ValueError: Module compiled against GDAL 3.6 but runtime is 3.8 Explicit osgeo ABI guard triggered by major.minor drift. Align GDAL_VERSION environment variable across build and deploy stages. Rebuild wheels against target runtime.
ImportError: ... GLIBC_2.32' not found manylinux platform tag mismatch. Wheel targets newer glibc than host OS. Rebuild using quay.io/pypa/manylinux2014_x86_64 or downgrade --plat tag in auditwheel.

Root Cause: The Two-Layer ABI Architecture

Python extension wheels encode two independent ABI layers that pip resolves separately. The first layer governs interpreter compatibility. Understanding the C-API vs CPython ABI Compatibility layer is critical: it dictates whether a .cpython-310-x86_64-linux-gnu.so binary can safely load under Python 3.10 without triggering symbol collisions or memory corruption.

The second layer is the native GDAL C-API. libgdal exposes versioned symbols (e.g., GDALOpenEx@GDAL_3.5) and enforces strict backward compatibility only within major releases. When pip resolves a wheel, it matches the CPython tag but completely ignores the native library ABI. If the deployment environment has a system gdal package installed, LD_LIBRARY_PATH or /etc/ld.so.conf often forces the dynamic linker to bypass the vendored .libs/ directory and load the system libgdal.so. This triggers symbol resolution failures, especially when the system library predates the wheel’s compilation target.

The broader implications of this architecture are documented in Geospatial C-Extension Fundamentals & ABI Architecture, which outlines how spatial extensions must isolate native dependencies to maintain deterministic CI/CD behavior.

Shared Library Resolution & RPATH Mechanics

manylinux wheels rely on auditwheel to rewrite RPATH entries, pointing the ELF loader to $ORIGIN/.libs/ where vendored libgdal.so and libproj.so reside. The PyPA binary distribution format strictly requires that wheels be self-contained and not depend on host system libraries.

Common resolution failures occur when:

  1. LD_LIBRARY_PATH overrides RPATH: The Linux dynamic linker prioritizes LD_LIBRARY_PATH over embedded RPATH. CI runners with system GDAL installed will silently hijack the import.
  2. Incomplete auditwheel repair: Missing --exclude flags for non-vendored dependencies (e.g., libcrypto, libcurl) cause auditwheel to skip repair, leaving unresolved system paths.
  3. Cross-architecture builds: Building on x86_64 and deploying to aarch64 without matching --plat tags leaves the ELF header pointing to incompatible glibc and symbol tables.

Pipeline Recovery & Exact Fix Mappings

1. Isolate the Build Environment

Never build spatial wheels on hosts with system GDAL installed. Use isolated Docker containers:

FROM quay.io/pypa/manylinux_2_28_x86_64:latest
ENV GDAL_VERSION=3.8.4
RUN yum install -y proj-devel sqlite-devel curl-devel zlib-devel
RUN pip install --no-cache-dir --upgrade pip setuptools wheel auditwheel

2. Force Vendoring During Build

Override system library resolution by explicitly setting RPATH during setup.py or pyproject.toml compilation:

export LDFLAGS="-Wl,-rpath,\$ORIGIN/.libs"
pip wheel --no-build-isolation -w dist/ .

3. Repair & Validate Platform Tags

Run auditwheel with explicit platform targeting to guarantee PyPA compliance:

auditwheel repair --plat manylinux_2_28_x86_64 \
  --exclude libcrypto.so.1.1 \
  --exclude libcurl.so.4 \
  dist/*.whl

Validation & Compliance Checks

After applying fixes, run this validation sequence before merging to production:

# 1. Verify embedded RPATH (extract first: readelf/ldd read ELF .so files, not .whl zips)
unzip -o -q wheelhouse/*.whl -d /tmp/wh
readelf -d /tmp/wh/osgeo/_gdal*.so | grep -i rpath
# Expected: $ORIGIN/.libs

# 2. Confirm no system library leakage
ldd /tmp/wh/osgeo/_gdal*.so | grep -v "not found" | grep -v "\.libs/"
# Expected: Empty output or only glibc/libpthread/libdl

# 3. Validate runtime ABI alignment
python -c "from osgeo import gdal; print(gdal.VersionInfo('RELEASE_NAME'))"
# Must match build-time GDAL_VERSION exactly

# 4. Check wheel metadata compliance
unzip -q -c wheelhouse/*.whl *.dist-info/WHEEL | grep Tag
# Must match: cp310-cp310-manylinux_2_28_x86_64

Preventive Architecture

To eliminate ABI drift in production pipelines:

  • Pin GDAL at the CMake level: Use find_package(GDAL 3.8.4 EXACT) in your build scripts.
  • Enforce abi3 for stable bindings: Where applicable, compile against the stable Python C-API (Py_LIMITED_API=1) to decouple interpreter upgrades from native recompilation.
  • Containerize deployment: Deploy wheels only in base images matching the manylinux builder glibc version. Cross-glibc deployments violate the PyPA manylinux specification and will fail unpredictably.
  • Automate ABI drift detection: Integrate ldd and auditwheel show into CI post-build hooks. Fail the pipeline if RPATH contains absolute paths or if libgdal.so resolves outside $ORIGIN/.libs.

By treating the GDAL extension as a self-contained binary artifact rather than a system dependency wrapper, teams achieve deterministic installs, eliminate runtime ImportError cascades, and maintain strict compliance with Python packaging standards.