Using conda-forge compilers for wheel builds
This page answers one question: you want a pinned, reproducible toolchain for compiling GDAL and your extension, and conda-forge ships compilers as ordinary packages — so how do you use them to build a wheel that does not depend on the conda prefix it was built in? It sits inside the Environment Isolation with Pixi and Conda section of the Modern Python Build Tooling & Wheel Configuration reference, and gives you the environment, the activation, and the repair step that severs the dependency.
Context & Root Cause
conda-forge publishes the C and C++ compilers, the target sysroot and the geospatial stack as packages, which makes a build environment expressible in a lock file: the same compiler, the same glibc floor and the same GDAL on a laptop and on a runner. That is a genuinely better starting point than installing a distribution’s build tools and hoping the versions match.
The catch is that the resulting wheel must not depend on any of it. Compiling against a conda prefix records absolute paths into that prefix, and a wheel carrying those references installs and then fails on any machine without the same environment. The build is a conda concern; the artifact must be an ordinary manylinux wheel. That separation is exactly what the repair step provides, and forgetting it is the single failure mode this approach has.
Solution / Fix
This targets conda-forge compilers with a manylinux-compatible sysroot, GDAL 3.8.x, auditwheel 6.x and scikit-build-core 0.9+.
1. Declare a build environment with the toolchain in it
# pixi.toml
[project]
channels = ["conda-forge"]
platforms = ["linux-64", "osx-arm64"]
[feature.build.dependencies]
c-compiler = "*"
cxx-compiler = "*"
"sysroot_linux-64" = "2.28.*" # sets the glibc floor for the wheel tag
cmake = ">=3.28"
ninja = "*"
gdal = "3.8.*"
proj = "9.3.*"
geos = "3.12.*"
scikit-build-core = ">=0.9"
auditwheel = ">=6"
[environments]
build = ["build"]
The sysroot pin is the load-bearing line: it is what fixes the glibc version the compiler targets, and therefore the platform tag the finished wheel can claim.
2. Build inside the environment
pixi run -e build python -m build --wheel --outdir dist/raw
Activation sets CC, CXX, CFLAGS and the sysroot flags, so the build finds the pinned toolchain without any of it being hard-coded in your project.
3. Repair the wheel out of the prefix
LD_LIBRARY_PATH="$(pixi run -e build printenv CONDA_PREFIX)/lib" \
pixi run -e build auditwheel repair --plat manylinux_2_28_x86_64 \
-w dist/ dist/raw/*.whl
LD_LIBRARY_PATH here tells the repair tool where to find the libraries it must copy in; without it the tool reports them as unresolvable external references.
4. Prove the artifact is independent
docker run --rm -v "$PWD/dist:/d" python:3.12-slim bash -c \
"pip install -q /d/*.whl && python -c 'from osgeo import gdal; print(gdal.__version__)'"
Verification
# 1. No path into the conda prefix survives in the wheel
unzip -o dist/*.whl -d /tmp/w >/dev/null
grep -rl "$CONDA_PREFIX" /tmp/w 2>/dev/null || echo "no prefix references"
readelf -d /tmp/w/**/*.so | grep -E 'RPATH|RUNPATH'
# expected: only $ORIGIN-relative entries
# 2. The platform tag matches the sysroot you pinned
auditwheel show dist/*.whl | grep -i 'platform tag'
# expected: manylinux_2_28_x86_64
# 3. It imports where no conda exists
docker run --rm -v "$PWD/dist:/d" python:3.12-slim bash -c \
"pip install -q /d/*.whl && python -c 'import geo_core; print(geo_core.VENDORED)'"
The first check is the one to automate. A single absolute path into a build prefix is enough to make a wheel unusable elsewhere, and it is invisible in every test run on the build machine.
What the conda Toolchain Actually Pins
The value of this approach is proportional to how much of the build it makes reproducible, so it is worth being precise about what the environment does and does not fix.
The two columns are complementary rather than alternatives. Pinning the environment without pinning the image leaves the kernel, the container’s own libc and any tool installed by the image floating; pinning the image without the environment leaves the compiler and libraries to whatever the image happens to ship. Doing both is what makes a build reproducible in the sense described in reproducible builds and supply-chain attestation.
One inclusion in the left column deserves emphasis: putting auditwheel inside the environment rather than installing it separately means the repair behaviour is pinned too. A repair tool that updates independently can change which libraries it bundles or how it computes a tag, and that is exactly the sort of unannounced change that makes a release differ from its predecessor for no visible reason.
When This Beats a manylinux Image, and When It Does Not
Both approaches produce valid wheels, and the choice depends on what the project’s constraints actually are.
The hybrid in the last line is what most mature spatial projects converge on. The image fixes the platform and carries the policy tooling; the conda environment inside it fixes the compiler and supplies a prebuilt GDAL so nothing spends twelve minutes compiling. The repair step then runs with the image’s auditwheel, which is the version the policy was written for.
Where the pure-conda approach shines on its own is local development and macOS, where there is no manylinux equivalent and a lock-file-defined toolchain is the most reproducible option available.
Pitfalls & Alternatives
Skipping the repair because “it works”. It works on the build machine, where the prefix exists. Repair is not an optimisation here; it is what converts an environment-specific build into a distributable artifact.
Mismatching the sysroot and the claimed tag. A sysroot_linux-64=2.17 environment producing a wheel labelled manylinux_2_28 claims less compatibility than it has; the reverse claims more than it has and fails on older systems. Let auditwheel compute the tag and check that it matches what you expected.
Installing the wheel into the build environment to test it. That environment has the libraries, so the test passes regardless of what the wheel contains. The clean container is the only honest check, as smoke-testing GDAL wheels in a clean container sets out.
Letting the build environment double as the test environment. Separate them in the manifest, as environment isolation with pixi and conda recommends; a test environment containing GDAL cannot prove the wheel carries its own.
Frequently Asked Questions
Does the wheel end up depending on conda in any way?
Not after repair. The bundled libraries were copied out of the prefix and their load paths rewritten to $ORIGIN, so the artifact is an ordinary wheel whose provenance happens to have been a conda environment. The verification step exists to make that a checked fact rather than an assumption.
Which sysroot version should I pin?
The one matching the platform tag you intend to publish — 2.28 for manylinux_2_28, 2.17 for manylinux2014. Pinning a newer sysroot than the tag produces a wheel that claims compatibility it does not have, which is the failure the tag computation is meant to catch.
Can I use this for macOS wheels?
Yes, and it is arguably more valuable there because there is no manylinux equivalent to standardise the toolchain. Pin the compilers and the deployment target in the environment, and use delocate in place of auditwheel.
Does it work with cibuildwheel?
It composes: run the conda environment setup in before-all and let the tool handle identifiers, repair and testing. The one thing to watch is that the environment’s variables reach the build backend, which under build isolation means listing them in the environment table rather than relying on activation.
Is the extra layer worth it if I already have a working manylinux build?
If the native stack is already prebuilt and cached, probably not — the marginal reproducibility gain is small. If your image builds GDAL from source on every run, replacing that with a pinned conda package removes the largest single cost in the pipeline, which is a different proposition entirely.
How do I keep the environment and the image from drifting apart?
Pin both by content — the lock file for the environment, the digest for the image — and bump them in separate, reviewed changes. Bumping both at once makes any resulting difference in the artifact impossible to attribute to one or the other.
Does the built wheel need the same conda environment to be tested?
Emphatically not — testing it there proves nothing, because the environment supplies the libraries the wheel is supposed to carry.
Related
- Environment isolation with pixi and conda — the three-environment split this build environment belongs to.
- Manylinux and manyarm Docker base images — what the image guarantees that a conda environment does not.
- Resolving GDAL conflicts between conda and PyPI — why the built wheel must not be installed back into the environment that built it.