Verifying wheel tags with auditwheel show

This page answers one question: how do you read the output of auditwheel show to confirm a spatial wheel carries a versioned manylinux tag and leaks no host libraries — before PyPI rejects it or a user hits a missing-SONAME error? It sits inside the Testing and Validating Spatial Wheels section of the Modern Python Build Tooling & Wheel Configuration reference, and gives you a line-by-line reading of the report, the tag that PyPI requires, and the external-reference check that catches unbundled dependencies.

Reading an auditwheel show report: the tag line and the external references The auditwheel show report has two parts that matter. The platform tag line must read a versioned manylinux tag such as manylinux_2_28_x86_64; a plain linux_x86_64 means unrepaired and PyPI rejects it. The external references section must list only permitted base libc libraries; any GDAL or PROJ entry there means that library was not bundled. tag line — must be versioned manylinux_2_28_x86_64 ✓ linux_x86_64 ✗ (unrepaired) PyPI rejects the plain tag external references — base only libc.so.6 · libm.so.6 ✓ libgdal.so.34 here ✗ GDAL listed = not bundled

Context & Root Cause

auditwheel show is a static inspector: it reads a wheel’s extension module, resolves what it links, and reports two things that decide whether the wheel is portable. The first is the platform tag it computes from the glibc symbol versions the binary references — this is the tag auditwheel repair would stamp, and the value PyPI checks. A wheel whose extension still references only base-system symbols and whose external libraries are all bundled earns a versioned tag like manylinux_2_28_x86_64; one that never went through repair keeps the generic linux_x86_64, which PyPI refuses because it makes no portability promise.

The second is the external references list: every shared library the extension needs that is not inside the wheel. For a correctly repaired geospatial wheel this list contains only the permitted base libc set (libc, libm, libpthread, libdl). If libgdal.so.34 or libproj.so.25 appears there, the repair did not bundle it, and the wheel will raise cannot open shared object file on any host lacking that exact SONAME. Reading these two sections is the static gate that precedes the runtime smoke test in Testing and Validating Spatial Wheels.

Solution / Fix

This targets auditwheel 6.x and a repaired Linux wheel. On macOS the analogue is delocate-listdeps --all.

1. Run the report

auditwheel show dist/_geospatial_ext-1.0-cp39-abi3-manylinux_2_28_x86_64.whl

A healthy report reads roughly:

_geospatial_ext-1.0-cp39-abi3-manylinux_2_28_x86_64.whl is consistent with
the following platform tag: "manylinux_2_28_x86_64".

The wheel references external versioned symbols in these system-provided
shared libraries: libc.so.6 with versions {GLIBC_2.17, ...}

This constrains the platform tag to "manylinux_2_28_x86_64".

2. Read the tag line

The tag must be versioned (manylinux_2_28, manylinux2014, or musllinux_1_2). If it says linux_x86_64, the wheel is unrepaired — run auditwheel repair and re-check.

3. Read the external references

# Any geospatial library in the external list is a bundling failure
auditwheel show dist/*.whl | grep -iE 'libgdal|libproj|libgeos'
# expected: empty  (they should be INSIDE the wheel, not external)

If a geospatial SONAME appears, re-run repair with LD_LIBRARY_PATH pointing at the build-time libraries, the fix in fixing “libgdal.so: cannot open shared object file”.

Verification

# 1. Tag is versioned manylinux/musllinux, never plain linux
auditwheel show dist/*.whl | grep -oE '(manylinux|musllinux)[_0-9a-z]*' | head -1
# expected: manylinux_2_28_x86_64 (a non-empty versioned tag)
# 2. No geospatial library is external
test -z "$(auditwheel show dist/*.whl | grep -iE 'libgdal|libproj|libgeos')" \
  && echo "all geospatial libs bundled"
# expected: all geospatial libs bundled
# 3. The tag in the report matches the filename tag
python - <<'PY'
import glob, re
f = glob.glob("dist/*.whl")[0]
print("filename tag:", re.search(r'-(manylinux[_0-9a-z]+|linux_\w+)\.whl', f).group(1))
PY
# expected: a manylinux tag matching the report, not linux_x86_64

A versioned tag, an empty geospatial-external check, and a filename that agrees with the report confirm the wheel is portable. A mismatch between filename and report tag means the file was renamed without a real repair.

Reading the Report Field by Field

auditwheel show prints four things, and each answers a question you would otherwise have to reconstruct from readelf output.

The four parts of an auditwheel show report and what each one means The report gives the tag currently on the filename, the tag the binary actually qualifies for, the list of external shared libraries still referenced, and the symbol versions that determined the qualification. A mismatch between the first two means the wheel has not been repaired or has been mislabelled. A non-empty external list means the wheel depends on the user's machine. The symbol versions name the single reference responsible when the computed tag is newer than expected. filename tag mypkg-1.0-cp39-abi3-manylinux_2_28_x86_64 what the file claims — set by whoever renamed it computed tag manylinux_2_28_x86_64 what the binary earns — derived from glibc symbols; must equal the line above external references libc.so.6, libm.so.6, libstdc++.so.6 anything beyond the base set is a library the user must supply — libcurl and libsqlite3 appear here often symbol versions GLIBC_2.28, GLIBC_2.17, CXXABI_1.3.9 names the single reference that set the floor — the line to read

The second and third rows carry almost all the signal. A computed tag that differs from the filename means either the wheel was never repaired or someone renamed it by hand, both of which produce an artifact that installs on machines it cannot run on. A non-empty external list beyond the base platform set means the wheel is not self-contained, however cleanly it imports on your machine.

When the Computed Tag Is Newer Than Expected

The most common surprise is a wheel built in a manylinux_2_28 image that reports a higher floor. The cause is always a reference to a symbol version the base image’s glibc does not define, and the symbol-version list names it.

How one symbol reference raises a wheel's entire glibc floor Most references in a wheel target older glibc versions, clustered around 2.17 and 2.28. A single reference to a symbol introduced in glibc 2.34 raises the computed floor for the whole wheel to 2.34, excluding every distribution below it. The usual sources are a library installed from the distribution's own repositories rather than compiled in place, and a build that linked against the runner's glibc instead of the image's. glibc 2.12 2.17 2.28 2.34 one reference at GLIBC_2.34 — often a pthread symbol computed floor for the whole wheel: 2.34 the fix is to find the object carrying that reference — usually one installed from a distribution repository rather than compiled in the image

Locating the offending object is mechanical: run readelf --dyn-syms over each bundled library and the extension, and grep for the version that set the floor. Whichever object references it is the one to rebuild inside the image, or to replace with a copy compiled against the base image’s toolchain.

Frequently Asked Questions

Why does my wheel show linux_x86_64 even though I ran the repair?

Because the repaired wheel is a new file in the output directory, and the original is still sitting where it was. Testing or uploading dist/*.whl after repairing into dist/repaired/ picks up the unrepaired original — the single most common version of this mistake, and the reason to make the two directory names impossible to confuse.

Is a higher manylinux tag always worse?

Not worse, narrower. manylinux_2_34 is a perfectly good tag if your users are all on current distributions, and it gives you a newer toolchain and standard library. It becomes a problem when it happens by accident, because then you have quietly dropped platforms nobody decided to drop.

What is the equivalent tool on macOS and Windows?

delocate-listdeps on macOS reports the install names a wheel still references, which is the analogue of the external-reference list; the deployment target plays the role of the glibc floor and is inspected separately. Windows has no equivalent at all, which is why the clean-environment import carries more weight there.

Should auditwheel show output be kept?

Yes — writing it into the release artifacts costs nothing and makes regressions diffable. When a release starts failing for users on an older distribution, comparing this release’s report with the previous one usually identifies the change in a single line, without reproducing the build.

Can I trust the tag on a wheel someone else built?

Only as far as you trust their pipeline. The tag is a filename component and nothing on an index recomputes it, so a mislabelled wheel installs happily and fails at import. Running the check yourself over third-party wheels you depend on is cheap, and for a package you vendor into a product it is worth doing once per upgrade.

What does it mean when the external-reference list is empty?

That the wheel references nothing outside the base platform set — which is the goal, and slightly suspicious if the package obviously needs, say, a C++ runtime. An empty list usually means everything was static-linked, which is fine, but confirm the extension actually loads before concluding the build did something clever.

Does the report change after the wheel is installed?

No, and that is worth knowing: the report describes the file, not the installation. If a wheel passes the tag check and still fails to import on a target machine, the cause is downstream of packaging — a loader precedence problem, a missing data file, or an interpreter mismatch — and the report has already done its job.

Should the check run before or after the repair step?

Both, and they answer different questions. Run before to see which external libraries the unrepaired wheel depends on, which tells you what the repair will have to bundle and whether anything unexpected crept into the link. Run after to confirm the tag and the remaining external list, which is the assertion that gates publication.

Pitfalls & Alternatives

Trusting the filename over the report. A wheel can be renamed to claim manylinux_2_28 without being repaired; auditwheel show computes the tag from the binary, so it is the source of truth. Always read the report, never just the filename.

Ignoring the external-symbol constraint. auditwheel may report that a newer glibc symbol constrains the tag to, say, manylinux_2_34, higher than you intended. That means GDAL was built against too-new a glibc — rebuild in the older manylinux_2_28 base image to lower the floor.

Treating show as sufficient. A perfect tag report does not prove the wheel runs — data files and driver registration are invisible to static inspection. Always follow with smoke-testing GDAL wheels in a clean container. Recording both reports as build artifacts also gives you a before-and-after pair to diff when a release behaves differently from the one before it.