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:
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:
LD_LIBRARY_PATHoverridesRPATH: The Linux dynamic linker prioritizesLD_LIBRARY_PATHover embeddedRPATH. CI runners with system GDAL installed will silently hijack the import.- Incomplete
auditwheel repair: Missing--excludeflags for non-vendored dependencies (e.g.,libcrypto,libcurl) causeauditwheelto skip repair, leaving unresolved system paths. - Cross-architecture builds: Building on
x86_64and deploying toaarch64without matching--plattags 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
abi3for 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
manylinuxbuilder glibc version. Cross-glibc deployments violate the PyPA manylinux specification and will fail unpredictably. - Automate ABI drift detection: Integrate
lddandauditwheel showinto CI post-build hooks. Fail the pipeline ifRPATHcontains absolute paths or iflibgdal.soresolves 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.