Fixing “libgdal.so: cannot open shared object file”
This page answers one question: your extension imported fine in CI but a user gets ImportError: libgdal.so.34: cannot open shared object file: No such file or directory, so how do you make the wheel carry its own GDAL instead of hoping the host has the right SONAME? It sits inside the Debugging Import Errors and Linker Failures cluster of the Geospatial C-Extension Fundamentals & ABI Architecture reference, and gives you the ldd diagnosis, the auditwheel repair fix, and the clean-container proof.
Context & Root Cause
cannot open shared object file means the dynamic loader walked its entire search path — RPATH, LD_LIBRARY_PATH, the default /etc/ld.so.cache, and the standard directories — and never found a file whose name matches the SONAME baked into your extension. The version suffix is the giveaway: libgdal.so.34 is GDAL’s ABI SONAME, and it exists only where GDAL 3.8/3.9 is installed. Your CI runner had it because the build image installed GDAL; the user’s python:3.12-slim container or fresh VM does not.
The root cause is therefore not the loader — it is doing exactly what it should — but a wheel that was never made self-contained. A distributable geospatial wheel must carry its native dependencies, because you cannot assume any given SONAME exists on the target. This is the vendoring contract from the parent reference: build, then repair so the loader finds libgdal next to the extension rather than on the host. The trade-offs of vendoring versus borrowing are laid out in vendoring PROJ and GDAL vs system libraries.
Solution / Fix
This targets auditwheel 6.x on Linux (delocate 0.11+ on macOS) and a wheel built against GDAL 3.8.x.
1. Confirm the missing SONAME with ldd
unzip -o dist/*.whl -d /tmp/w >/dev/null
ldd /tmp/w/*.so | grep -i gdal
# symptom: libgdal.so.34 => not found
=> not found confirms class A (missing SONAME) rather than an undefined-symbol or precedence problem.
2. Repair the wheel so GDAL is bundled
auditwheel repair inspects the external links, copies each required .so into a .libs/ directory inside the wheel, and rewrites the extension’s RPATH to $ORIGIN so the loader looks next to it:
# LD_LIBRARY_PATH must point at the build-time GDAL so auditwheel can find
# the library it needs to copy in.
LD_LIBRARY_PATH=/opt/gdal/lib \
auditwheel repair --plat manylinux_2_28_x86_64 -w dist/repaired/ dist/*.whl
3. Confirm the repaired wheel is self-contained
unzip -o dist/repaired/*.whl -d /tmp/r >/dev/null
ls /tmp/r/*.libs/ # expect libgdal-<hash>.so.34, libproj..., libgeos...
readelf -d /tmp/r/*.so | grep RUNPATH
# expect: [$ORIGIN/../<pkg>.libs]
The $ORIGIN/../<pkg>.libs RUNPATH is what makes the wheel relocatable — the exact loader mechanics are covered in managing shared library paths in manylinux.
Verification
# The only test that counts: import in a container with NO gdal installed
docker run --rm -v "$PWD/dist/repaired:/d" python:3.12-slim \
bash -c "pip install /d/*.whl && python -c 'from osgeo import gdal; print(gdal.__version__)'"
# expected: 3.8.x (no 'cannot open shared object file')
# And the platform tag must be versioned, never plain 'linux'
auditwheel show dist/repaired/*.whl | grep -i platform
# expected: manylinux_2_28_x86_64
A pass shows a version string printed from a bare image. If it still raises cannot open shared object file, the repair did not run against this wheel — confirm you installed the file from dist/repaired/, not the original dist/.
Pitfalls & Alternatives
Setting LD_LIBRARY_PATH on the target as a “fix”. Exporting LD_LIBRARY_PATH=/path/to/gdal on the user’s machine makes the import succeed and hides the real defect — the wheel is still not portable, and the next user hits the same wall. Repair the wheel; do not patch the environment.
Repairing with the wrong LD_LIBRARY_PATH at build time. If auditwheel cannot locate the build-time libgdal, it reports cannot find libgdal.so.34 during repair. Point LD_LIBRARY_PATH at the directory holding the GDAL you linked against.
Bundling GDAL but forgetting proj.db. GDAL loads, then PROJ raises a data error because the datum database was not packaged — a different failure covered in bundling proj.db datum grids in a wheel.
Related
- Debugging Import Errors and Linker Failures — the parent guide that classifies this error against undefined-symbol and version-mismatch failures.
- Vendoring PROJ and GDAL vs system libraries — why a distributable wheel must carry its own
libgdal. - Managing shared library paths in manylinux — how the
$ORIGINRUNPATH that repair writes resolves at load time.