pixi vs conda-lock for reproducible spatial envs

This page answers one question: for a reproducible GDAL/PROJ build environment, should you adopt pixi — an all-in-one project manager with a unified multi-platform lock — or conda-lock, which pins an existing environment.yml into per-platform lock files, and where does each fit an existing conda workflow versus a greenfield project? It sits inside the Dependency Resolution and Lockfiles section of the Modern Python Build Tooling & Wheel Configuration reference, and gives you a decision table, the migration cost, and the CI ergonomics of each.

pixi and conda-lock compared across five reproducibility concerns Across lock scope, PyPI integration, task running, existing-workflow fit, and CI setup, pixi provides a unified multi-platform lock, native PyPI plus conda solving, a built-in task runner, and a greenfield fit, while conda-lock produces per-platform locks from an existing environment.yml, integrates PyPI weakly, has no task runner, and fits an established conda workflow with minimal change. pixi conda-lock lock scope PyPI + conda task runner existing env.yml CI setup unified multi-platform native, one solve built in adopt / migrate single action per-platform files weak none drop-in pin conda + step

Context & Root Cause

Both tools produce a committed lock that pins the native spatial stack, but they start from opposite ends. conda-lock is a pinning tool: you keep your existing environment.yml, and conda-lock resolves it into explicit per-platform lock files (conda-linux-64.lock, etc.) that conda/mamba install verbatim. It changes nothing about how you already work — it just freezes it. pixi is a project manager: it introduces its own pixi.toml manifest and pixi.lock, solves conda and PyPI dependencies in one pass, adds a task runner, and manages the environment lifecycle end to end.

For geospatial builds the decision usually comes down to two questions: do you already have a conda workflow you want to keep, and do you need PyPI-only packages (like a niche pyproj build) solved together with conda’s GDAL? An established team with a working environment.yml gets reproducibility fastest with conda-lock; a new project, or one that wants a single tool for environments, tasks, and locking, is better served adopting pixi. This is the tool-choice refinement of the concepts in Dependency Resolution and Lockfiles.

The decision

Concern pixi conda-lock
Manifest New pixi.toml Reuses environment.yml
Lock format One unified pixi.lock (all platforms) One file per platform
conda + PyPI Solved together in one graph conda-first; PyPI weaker
Task runner Built in (pixi run) None (bring your own)
Migration cost Adopt a new manifest Near zero — pin what you have
CI setup-pixi action, one step conda + a conda-lock install step
Best fit Greenfield / all-in-one Existing conda workflow to freeze

Choose conda-lock when you have a working conda environment and only want to make it reproducible with minimal change. Choose pixi when you are starting fresh, want conda and PyPI resolved in one lock, or value the integrated task runner for driving pixi run build-wheel. Neither is more “correct” — they optimize for migration cost versus integration.

Setup side by side

conda-lock — freeze an existing environment

# environment.yml already exists; pin it for three platforms
conda-lock lock -f environment.yml -p linux-64 -p osx-arm64 -p win-64
git add conda-*.lock
# install verbatim in CI — no re-solve
conda-lock install --name build conda-linux-64.lock

pixi — adopt a project manifest

pixi init && pixi add "gdal>=3.8,<3.9" "proj>=9.3,<9.4" python
pixi lock                 # writes the unified pixi.lock
git add pixi.toml pixi.lock
pixi install --locked     # install from the lock in CI

Both then feed the same wheel build, which still needs auditwheel/delocate repair as configuring pixi environments for wheel building shows.

Verification

# 1. conda-lock: the lock installs and pins exact GDAL
conda-lock install --name t conda-linux-64.lock && conda list -n t gdal
# expected: a single resolved gdal build string
# 2. pixi: the locked install is current and pins the same
pixi install --locked && pixi list | grep -E 'gdal|proj'
# expected: single resolved versions, install succeeds
# 3. Either tool: the same version resolves across platforms
grep -oE '3\.8\.[0-9]+' pixi.lock conda-*.lock 2>/dev/null | sort -u
# expected: one GDAL patch version, not several

What Each Tool Puts in the Lock

The two tools solve the same problem with different lock shapes, and the shape is what determines how the lock behaves in review, in CI and across platforms.

A unified pixi lock compared with per-platform conda-lock files On the left, pixi writes a single lock file containing a section per platform and per named environment, covering both conda and PyPI packages. On the right, conda-lock writes one explicit file per platform, each listing conda packages only, with PyPI dependencies handled separately. The unified file gives one diff across all platforms; the per-platform files give simpler CI wiring at the cost of keeping several artifacts in step. pixi.lock — one file environments: default: [linux-64, osx-arm64, win-64] build: [linux-64, osx-arm64, win-64] test: [linux-64] packages: conda packages with build strings PyPI wheels with hashes, in the same file one diff shows every platform at once conda-*.lock — one per platform conda-linux-64.lock explicit URLs + hashes, conda only conda-osx-arm64.lock solved independently conda-win-64.lock PyPI dependencies handled separately one file per CI job — simple wiring, several artifacts to keep in step the choice is mostly about which failure you would rather have: a large single diff, or three files that can drift apart

For a project whose build environment also needs PyPI-only packages — a build backend, a repair tool, a linter — the unified lock has a real advantage: those dependencies are pinned in the same solve rather than installed afterwards with whatever pip resolves that day. For a project that already has an environment.yml and a working CI wiring, the per-platform files are the smaller change.

How Each Behaves in CI

The daily difference between the two shows up not in the lock format but in the three commands a pipeline runs.

The CI shape of a pixi project compared with a conda-lock project Three steps compared. Installing the environment is one command in pixi that refuses to run against a stale lock, and a create-from-explicit-file command in conda-lock that has no staleness check. Running a task is a pixi task invocation that carries the environment, or an activate followed by a command. Caching is keyed on the single lock hash for pixi and on the per-platform file for conda-lock. A note records that the staleness check is the practical difference. pixi conda-lock install pixi install --locked fails if the lock is stale conda create --file …lock no staleness check run pixi run build-wheel the task carries its environment conda activate && … activation must be scripted per shell cache key: hash(pixi.lock) one key for every platform key: hash(conda-$plat.lock) one key per platform the staleness check is the difference that matters day to day: it converts silent drift into a failed job

That last point is the strongest practical argument in the comparison. A lock file only guarantees reproducibility if something notices when it stops matching the manifest, and a tool that refuses to install a stale lock provides that guarantee without anyone having to remember it.

Pitfalls & Alternatives

Adopting pixi mid-flight for a large conda team. Migrating a working environment.yml to pixi.toml has a real cost in retraining and CI rewiring. If reproducibility is the only goal, conda-lock reaches it without the migration — reserve pixi adoption for when its task runner and unified lock earn their keep.

Expecting conda-lock to solve PyPI-only packages well. conda-lock is conda-first; a package available only on PyPI is a weak spot. If your GDAL build depends on such packages, pixi’s unified solve avoids the pip-on-top-of-conda hazard warned about in configuring pixi environments for wheel building.

Re-solving in CI with either tool. The whole point is to install from the committed lock. Running a fresh solve on the runner reintroduces the drift the lock exists to prevent — use --locked (pixi) or conda-lock install (never a bare conda env create).

Frequently Asked Questions

Which should a brand-new spatial project choose?

The unified-lock tool, unless something specific argues otherwise. A new project has no existing environment.yml to preserve, benefits from having PyPI and conda dependencies resolved in one solve, and gains the staleness check for free. The per-platform tool remains the better answer when you are pinning an environment file that already exists and is consumed by things outside your control.

Can the two coexist during a migration?

They can, for a while, and the risk is that they diverge. If both are present, make one authoritative — the one CI installs from — and treat the other as generated output that is regenerated rather than edited. A migration that leaves two hand-maintained locks in the repository produces environments that differ in ways nobody notices until a build behaves differently from a developer’s machine.

Do either of them pin the compiler toolchain?

Both can, because conda-forge ships the compilers as ordinary packages. Adding c-compiler, cxx-compiler and the matching sysroot to the manifest puts the toolchain under the same pin as GDAL and PROJ, which closes the last unpinned input in a wheel build. Without it, a runner image upgrade can change the compiler under a lock that claims the environment is reproducible.

How do they handle PyPI-only build dependencies?

The unified lock resolves them in the same solve and records hashes, so the build backend and repair tools are pinned exactly like the native packages. The per-platform approach handles conda packages and leaves PyPI dependencies to a separate mechanism, typically a requirements file installed afterwards — workable, but it means part of the environment is pinned by one tool and part by another.

What happens when a lock cannot be solved for one platform?

Both tools fail for that platform and succeed for the others, which is the right behaviour but easy to miss in a long log. Solve each platform explicitly during investigation so the failing one is obvious, and expect the cause to be a package with no build for that platform rather than a version conflict — the usual answer is to make that dependency conditional rather than universal.

Is a lock file enough to make a wheel reproducible?

It makes the environment reproducible, which is necessary and not sufficient. The wheel also depends on the compiler flags, the base image and the repair step, none of which the lock describes. Pinning the image by digest and keeping the build commands in a script that both CI and developers invoke closes the remaining gaps.

Does either tool help with the container the build runs in?

Neither pins the image, which is the gap worth noticing. A scrupulously locked environment inside a container referenced by a floating tag is only half reproducible: the packages are fixed and the toolchain underneath them is not. Pin the image by digest alongside the lock, and treat the two as a pair that moves together.