Dependency Resolution and Lockfiles for Spatial Environments
Reproducibility in a geospatial build stands or falls on the lock file: the exact GDAL, PROJ, GEOS, and interpreter versions must be pinned cryptographically, or the same pyproject.toml resolves to a different native ABI on every machine and the wheels drift. This guide sits under the Modern Python Build Tooling & Wheel Configuration reference and covers how solvers pin the native spatial stack — conda-style resolution, the lock-file formats, multi-platform locking, and the discipline that keeps a build environment identical from a laptop to a CI runner. It targets pixi 0.30+, conda-lock 2.5+, conda/mamba, conda-forge GDAL 3.8.x/PROJ 9.3.x, and complements the isolation model in Environment Isolation with Pixi and Conda.
Prerequisites & Environment
- A solver:
pixi0.30+ (which embeds a fast resolver) orconda-lock2.5+ overconda/mamba. - The conda-forge channel with
channel-priority: strict— mixing channels is the top cause of an unsolvable or ABI-inconsistent spatial environment. - The set of target platforms declared up front (
linux-64,osx-arm64,win-64), because a lock is only reproducible on platforms it was solved for.
# Confirm a solver is available
pixi --version || conda-lock --version
Core Configuration
A lock file records three things a loose manifest cannot: the exact build string of every native package (so libgdal 3.8.5 h... _0 is pinned, not just 3.8), a content hash for verification, and a per-platform solution so osx-arm64 and linux-64 each get a coherent set. The two dominant tools differ in scope:
| Tool | Lock format | Scope | Best for |
|---|---|---|---|
pixi |
pixi.lock (multi-platform, unified) |
conda + PyPI in one solve, task runner | Project-local build/dev environments |
conda-lock |
conda-<plat>.lock per platform |
conda packages, renders to explicit specs | Pinning an existing environment.yml for CI |
The head-to-head trade-offs — unified versus per-platform locks, PyPI integration, and CI ergonomics — are worked through in pixi vs conda-lock for reproducible spatial envs. Either way, the lock is the artifact you commit and install from; the manifest is only its input.
Step-by-Step Implementation
-
Declare ranges and channels in the manifest, pinning the native minor versions so the ABI is stable:
# pixi.toml (excerpt) channels = ["conda-forge"] platforms = ["linux-64", "osx-arm64", "win-64"] [dependencies] gdal = ">=3.8,<3.9" proj = ">=9.3,<9.4" python = ">=3.10,<3.13" -
Solve once and commit the lock:
pixi lock # writes pixi.lock for every declared platform git add pixi.lock # the lock is a committed artifact, not a build output -
Install from the lock everywhere — never re-solve in CI:
pixi install --locked # fails if pixi.lock is stale, guaranteeing fidelity -
Regenerate deliberately. Update the lock only via an explicit
pixi update, reviewed like any dependency bump, so native versions never change by accident.
Verification
# 1. The locked environment is byte-consistent with the manifest
pixi install --locked && echo "lock is current"
# expected: lock is current (non-zero exit if pixi.lock drifted)
# 2. The exact native versions are pinned, not just ranges
grep -E 'gdal|proj' pixi.lock | grep -oE '3\.[0-9]+\.[0-9]+' | sort -u | head
# expected: single resolved versions like 3.8.5 and 9.3.1
# 3. The same lock solves for every target platform
grep -c 'platform:' pixi.lock
# expected: one entry per declared platform (linux-64, osx-arm64, win-64)
Matching versions across a --locked install and one resolved GDAL/PROJ version per platform confirm the environment is reproducible.
Optimization & Edge Cases
- Cache the solved environment in CI. Restoring a materialized prefix keyed on the lock hash cuts install time dramatically — the caching pattern in async build execution and cache strategies.
- Lock covers the build, not the wheel. A conda lock pins the build environment; the distributable wheel still needs
auditwheel/delocaterepair to be relocatable off that environment. - Pin
libgdalexplicitly, not justgdal. The Pythongdalpackage and the Clibgdalcan float independently on conda-forge; pin both to keep the ABI fixed.
Troubleshooting
LibMambaUnsatisfiableError on solve. A channel mix or an over-tight pin made the graph unsolvable. Set channel-priority: strict, loosen the least-important pin, and re-solve.
A --locked install fails after editing the manifest. That is the guardrail working: the lock is stale. Run pixi update (or conda-lock) intentionally and commit the new lock.
Different results on macOS vs Linux. The lock was solved for only one platform. Declare every target platform before solving so the lock carries all of them.
Related
- pixi vs conda-lock for reproducible spatial envs — the tool-choice decision with a side-by-side comparison.
- Environment Isolation with Pixi and Conda — the isolation model the lock file pins.
- Async build execution and cache strategies — caching the materialized environment keyed on the lock hash.
Further Reading
pixiandconda-lockdocumentation for the authoritative lock-format semantics.