-
Notifications
You must be signed in to change notification settings - Fork 1
Motivation
nepenthe exists to make large, shared software environments reproducible, fast to install, and safe to evolve — across many machines, many repositories, and time. This page explains the problem it solves and why the common alternatives fall short for that job.
To behave consistently across many machines and over time, you generally want frozen environments — an exact, pinned set of packages that installs the same way everywhere. But pinning everything by hand makes upgrades miserable and quickly leads to dependency hell: change one pin and you may have to re-pin a cascade of others.
nepenthe starts from a list of dependencies where you pin only what you must — the things you actively care about or know have compatibility issues. Each release solves once and freezes the result into a fully-pinned collection.
Every download of that release gives the same pinned packages, installed without re-solving — so installs are fast and identical on every machine.
You evolve an environment by editing its root dependencies, not the solved set. Each change produces a new frozen environment; old releases are never regenerated, so they stay stable forever.
Frequent releases provide upgrade paths, and teams move from older environments to newer ones in whatever increment and on whatever timeline fits their needs.
The crucial distinction: most tools are project-focused — they lock the dependencies of one repository. nepenthe is environment-focused — it builds and distributes a shared environment that many repositories consume. That difference is why the usual options don't fit this job.
Tools like pixi (pixi.lock) and uv (uv.lock) are excellent at what
they do: lock the dependencies of a single project so its developers get a fast,
reproducible setup. nepenthe deliberately reuses the same building blocks
(rattler, the rattler_lock format) — but they solve the opposite half of the
problem:
- A lock is a property of one project. Locks from different repos are independent and aren't meant to be merged — combining two repos' environments may not even solve.
- There's no notion of a large shared environment (often the union of many teams' and projects' needs) that is solved once, versioned, and distributed centrally for many repos to install.
- Every repository re-derives and re-locks its own dependencies; there's no single blessed environment that fifty repos install and roll forward together (optionally, at their own pace).
- uv is Python-only — it can't manage the conda/C/C++/CUDA/native packages that scientific and ML stacks depend on.
pixi locks a repo's environment; nepenthe produces and distributes the shared environments that repos consume. The two are complementary.
A conda environment.yml is a manifest, not a lock. conda env create -f environment.yml re-solves every time, which has two consequences:
- Not reproducible. Different machines, or the same machine at a different time, can get a different package set as channels evolve.
- Slow. You pay for a full solve on every install.
The usual workaround — hand-maintaining a huge, fully-pinned environment.yml —
is brittle: editing one pin can force you to re-pin a cascade of transitive
dependencies (dependency hell again). And a manifest gives you no independent
versioning, no immutability, no content-addressed integrity, and no
"solve once, distribute everywhere."
nepenthe keeps the friendly input (a short, mostly-unpinned dependency list) but turns it into a frozen, versioned, installable artifact that never re-solves on install.
These are great within Python, but they're limited to Python. The moment a package needs a C/C++/Rust library, CUDA, or another native dependency — which is most of a real scientific or ML stack — you fall back to something else to provide it. They can't manage the whole environment.
These handle C/C++/Rust well, but integrating them with Python is awkward, and nix/guix in particular carry a steep learning curve and bespoke ecosystems that are hard to adopt across an organization. nepenthe leans on the conda ecosystem instead, which spans Python and native code and already models hard cases like gRPC and CUDA.
System package managers change the machine globally: you can't easily run multiple versions of an environment side by side, reverting is disruptive, and nothing is portable to other hosts. That makes parallel production deployments and air-gapped/colo installs painful — exactly the cases nepenthe targets.
Container images are heavyweight and opaque: they rebuild per change and aren't a
lightweight, installable environment you can diff, activate, or share a
package cache across. They're complementary, not competing — a nepenthe lock can
be baked into an image, giving the image a reproducible, auditable
environment.
| Property | What nepenthe gives you |
|---|---|
| Multi-language | The conda ecosystem — Python, C, C++, Rust, CUDA, native libs — in one environment |
| Solve once, freeze | Reproducible everywhere and fast to install; installs never re-solve |
| Environment-focused | A shared collection many repos consume, not a per-repo lock |
| Independently versioned | Each environment has its own semver sequence — no global stamp, no filename encoding |
| Immutable & content-addressed | A published version never changes; rollback repoints a label; pulls are integrity-checked |
| Portable & isolated | Install in parallel, swap a symlink to revert, sync wholesale to restricted networks |
| Backend-agnostic | Specs and locks live on any backend (file://, s3://, https://); channels point at any server |
| Install without conda | A single binary links the lock into a prefix — no conda/mamba/micromamba on the target |
- Concepts — the vocabulary (manifests, features, variants, locks, registry).
- Manifests — author an environment.
- Registry & Versioning — publish and resolve versions.
- Installing Environments — create a prefix from a lock.
This wiki is autogenerated. To made updates, open a PR against the original source file in docs/wiki.