# conda-express (cx) design document
## What is conda-express?
conda-express (cx) is a lightweight, single-binary bootstrapper for conda, written in Rust using the [rattler](https://github.com/conda/rattler) crate ecosystem. It replaces the miniconda/constructor install pattern with a ~10 MB static binary that can install a fully functional conda environment in seconds.
Inspired by UV's single-binary distribution model, cx aims to be the fastest way to get a working conda installation.
## Current status
**Working PoC** — all core functionality is implemented and tested on macOS ARM64.
| Single-binary bootstrapper | Done |
| Compile-time lockfile (rattler-lock v6) | Done |
| Post-solve package exclusion | Done |
| conda-libmamba-solver removal | Done |
| conda-rattler-solver as default | Done |
| conda-spawn activation model | Done (installed) |
| `cx shell` alias for `conda spawn` | Done |
| Disabled commands (`activate`, `deactivate`, `init`) | Done |
| Auto-bootstrap on first `conda` command | Done |
| `.condarc` with `solver: rattler` | Done |
| External lockfile override (`--lockfile`) | Done |
| Live solve fallback (`--no-lock`) | Done |
| Multi-platform CI (via pixi) | Done |
| Release binary builds | Done |
| CEP 22 frozen base prefix | Done |
| Self-update (via conda-self plugin) | Not started |
| Reusable GitHub Action | Not started |
### Numbers (macOS ARM64, debug/release)
| Release binary size | ~10 MB |
| Installed packages (base) | 86 |
| Excluded packages (libmamba tree) | 27 |
| Bootstrap time (embedded lockfile) | ~3–5 s |
| Bootstrap time (live solve) | ~7–8 s |
| Lockfile size | ~1050 lines (rattler-lock v6) |
## Architecture
```
pixi.toml [tool.cx]: packages, channels, excludes
|
v
build.rs Compile-time: solve + filter + write lockfile
|
v
cx.lock rattler-lock v6 (embedded via include_str!)
|
v
cx Single binary (~10 MB release)
|
+---> bootstrap -----> install from lockfile (fast path)
| or live solve (fallback)
| write CEP 22 frozen marker
|
+---> info -------------> show prefix metadata
|
+---> shell -----------> alias for `conda spawn` (activate via subshell)
|
+---> activate/deactivate/init --> disabled (guides to conda-spawn)
|
+---> <any conda arg> --> hand off to installed conda binary
| (includes `conda self update` via conda-self)
```
### Compile-time lockfile
`build.rs` performs the full solve at `cargo build` time:
1. Reads `[tool.cx]` from `pixi.toml` (packages, channels, excludes)
2. Hashes the config; skips solve if cached lockfile matches
3. Fetches repodata via `rattler_repodata_gateway` (sharded)
4. Solves via `rattler_solve` (resolvo)
5. Filters out excluded packages and their exclusive dependencies
6. Writes a rattler-lock v6 lockfile to `$OUT_DIR/cx.lock`
7. Binary embeds it via `include_str!`
At runtime, bootstrap parses the embedded lockfile, extracts `RepoDataRecord`s, and passes them directly to `rattler::install::Installer` — no repodata fetch, no solve.
### Package exclusion
conda on conda-forge hard-depends on `conda-libmamba-solver`. Since cx uses `conda-rattler-solver` instead, it removes libmamba and its 27 exclusive native dependencies (libsolv, libarchive, libcurl, spdlog, etc.) via a post-solve transitive dependency pruning algorithm. This happens both at compile time (in `build.rs`) and optionally at runtime (for live solve or external lockfiles).
### Disabled commands
cx intercepts three conda commands that conflict with the conda-spawn activation model:
- **`activate` / `deactivate`** — prints a message directing users to `conda spawn` instead.
- **`init`** — explains that shell profile modifications are unnecessary; guides the user to add `condabin` to their PATH.
These commands exit with a non-zero status to prevent scripts from silently succeeding.
### Process hand-off
When cx receives a command it doesn't own (anything other than `bootstrap`, `info`, or a disabled command), it replaces its own process with the installed `conda` binary using the Unix execvp syscall. This means conda's full feature set is available transparently — cx is invisible after bootstrap.
### Frozen base prefix (CEP 22)
After bootstrap, cx writes a `conda-meta/frozen` marker file per [CEP 22](https://conda.org/learn/ceps/cep-0022/). This protects the base prefix from accidental modification — users should create named environments for their work. Updating the base installation is handled by `conda self update` (via conda-self), which internally overrides the frozen check.
## File structure
```
conda-express/
Cargo.toml Rust project manifest (crate: conda-express, binary: cx)
pyproject.toml maturin config for PyPI wheel builds
pixi.toml Dev environment + [tool.cx] package config + docs deps
pixi.lock Locked dev dependencies
build.rs Compile-time solver and lockfile generator
LICENSE BSD 3-Clause
README.md User-facing documentation
DESIGN.md This file
PLAN.md Feasibility analysis and implementation plan
src/
main.rs Entry point, command dispatch, disabled commands
cli.rs CLI definitions (clap)
config.rs Embedded config, prefix metadata, .condarc, CEP 22 frozen
install.rs Package installation (lockfile + live-solve paths)
exec.rs Process replacement (exec into installed conda)
python/
conda_express/
__init__.py Exposes find_cx_bin()
__main__.py python -m conda_express -> exec cx
_find_cx.py Locate cx binary in sysconfig paths
py.typed PEP 561 type marker
docs/
conf.py Sphinx config (conda-sphinx-theme, MyST)
index.md Homepage with install tabs, grid cards
quickstart.md Installation and first steps
features.md Feature descriptions
configuration.md Build-time and runtime config reference
design.md Includes DESIGN.md via MyST include
changelog.md Release notes
reference/
cli.md CLI reference
.github/
workflows/
ci.yml CI: build, test, lint on all platforms (canary artifacts)
release.yml Release: build + upload binaries to GitHub Releases
publish-pypi.yml Publish platform wheels to PyPI (trusted publishing)
publish-crates.yml Publish crate to crates.io (trusted publishing)
```
## Development environment
cx uses [pixi](https://pixi.sh) to manage the Rust toolchain from conda-forge, ensuring consistent builds across local development and CI:
```bash
# Install pixi (if not already installed)
# Build, test, lint
pixi run build # cargo build --release
pixi run test # cargo test
pixi run lint # fmt-check + clippy
```
The `pixi.toml` pins `rust >= 1.85` from conda-forge. CI workflows use `prefix-dev/setup-pixi` to replicate the same environment on all platforms.
## Configuration
The `[tool.cx]` section in `pixi.toml` is the single source of truth for what gets installed:
```toml
[tool.cx]
channels = ["conda-forge"]
packages = [
"python >=3.12",
"conda >=25.1",
"conda-rattler-solver",
"conda-spawn",
"conda-pypi",
"conda-self",
]
exclude = ["conda-libmamba-solver"]
```
Both `build.rs` (compile-time) and the runtime binary read from `pixi.toml`. Changing it triggers an automatic re-solve on the next `cargo build`.
## CLI
```
cx bootstrap [--force] [--prefix DIR] [--channel CH] [--package PKG]
[--exclude PKG] [--no-exclude] [--no-lock] [--lockfile PATH]
cx info [--prefix DIR]
cx shell [ENV] # alias for conda spawn (activate via subshell)
cx <any-conda-command> # transparently delegates to conda
```
Default prefix: `~/.cx`
### Disabled commands
| `cx activate` | Prints guidance to use `conda spawn` instead |
| `cx deactivate` | Prints guidance to use `conda spawn` instead |
| `cx init` | Explains `conda init` is unnecessary with conda-spawn |
## Default installed plugins
| conda-rattler-solver | Rust-based solver (replaces libmamba) |
| conda-spawn | Subprocess-based activation (replaces `conda activate`) |
| conda-pypi | PyPI interoperability (install, solve, convert) |
| conda-self | Base environment self-management |
## Lockfile compatibility
The embedded `cx.lock` is a standard rattler-lock v6 file, compatible with:
- pixi (same lockfile format)
- conda-lockfiles (`RattlerLockV6Loader`)
- Version control (can be checked in for audit)
## Key dependencies
All from the [rattler](https://github.com/conda/rattler) ecosystem:
| `rattler` | Package installation engine |
| `rattler_solve` (resolvo) | SAT-based dependency solver |
| `rattler_repodata_gateway` | Repodata fetching (sharded) |
| `rattler_conda_types` | conda type definitions |
| `rattler_lock` | Lockfile read/write (v6 format) |
| `rattler_virtual_packages` | Virtual package detection |
| `rattler_networking` | Auth middleware, OCI support |
| `rattler_cache` | Cache directory management |
## PyPI distribution
cx is published to PyPI as platform wheels via [maturin](https://github.com/PyO3/maturin) (`bindings = "bin"`), following the same pattern as [uv](https://github.com/astral-sh/uv). A tiny Python wrapper in `python/conda_express/` provides:
- `find_cx_bin()` — locates the binary via sysconfig
- `python -m conda_express` — finds and exec's the cx binary
## CI/CD
All workflows use `pixi` for toolchain management:
- **`ci.yml`** — runs on push to `main` and PRs. Builds and tests across 5 targets (linux-x64, linux-aarch64, macos-x64, macos-arm64, windows-x64). Uploads canary binaries as artifacts. Runs `pixi run lint` separately.
- **`release.yml`** — triggers on GitHub Release publication. Builds release binaries, computes SHA-256 checksums, and uploads both to the release.
- **`publish-pypi.yml`** — triggers on GitHub Release publication. Builds platform wheels via maturin-action, then publishes to PyPI using trusted publishing (OIDC, no API tokens).
- **`publish-crates.yml`** — triggers on GitHub Release publication. Publishes the crate to crates.io using trusted publishing via `rust-lang/crates-io-auth-action`.
## Future work
- **conda-self updater plugin**: Pluggable backend for conda-self so `conda self update` can delegate to cx/rattler for cx-managed prefixes (handles post-solve exclusion of libmamba). This is the canonical update path — cx intentionally does not implement its own update command.
- **GitHub Action**: Reusable action to build custom cx binaries with configurable package lists.
- **Upstream conda-forge**: Make `conda-libmamba-solver` an optional dependency of conda on conda-forge, eliminating the need for post-solve exclusion entirely.