Design#
What is conda-express?#
conda-express (cx) is a lightweight, single-binary bootstrapper for conda, written in Rust using the rattler crate ecosystem. It replaces the miniconda/constructor install pattern with a ~17 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.
Feature |
Status |
|---|---|
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) |
|
Done |
Disabled commands ( |
Done |
Auto-bootstrap on first |
Done |
|
Done |
External lockfile override ( |
Done |
Live solve fallback ( |
Done |
Multi-platform CI (via pixi) |
Done |
Release binary builds |
Done |
CEP 22 frozen base prefix |
Done |
|
Done |
Output filtering (create/env create) |
Done |
Installer scripts (get-cx.sh, get-cx.ps1) |
Done |
|
Done |
Reusable GitHub Action / composite action |
Done |
Build-time env var overrides ( |
Done |
Homebrew formula (same-repo tap) |
Done |
Self-update (via conda-self plugin) |
Not started |
Numbers (macOS ARM64, debug/release)#
Metric |
Value |
|---|---|
Release binary size |
~17 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 (~17 MB release)
|
+---> bootstrap -----> install from lockfile (fast path)
| or live solve (fallback)
| write CEP 22 frozen marker
|
+---> status -----------> show cx prefix metadata
|
+---> shell -----------> alias for `conda spawn` (activate via subshell)
|
+---> uninstall -------> remove prefix, envs, binary, PATH entries
|
+---> help -----------> clap auto-generated help with quick start
|
+---> 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:
Reads
[tool.cx]frompixi.toml(packages, channels, excludes)Applies environment variable overrides if set (
CX_PACKAGES,CX_CHANNELS,CX_EXCLUDE)Hashes the config (including overrides); skips solve if cached lockfile matches
Fetches repodata via
rattler_repodata_gateway(sharded)Solves via
rattler_solve(resolvo)Filters out excluded packages and their exclusive dependencies
Writes a rattler-lock v6 lockfile to
$OUT_DIR/cx.lockBinary embeds it via
include_str!
At runtime, bootstrap parses the embedded lockfile, extracts RepoDataRecords, 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 toconda spawninstead.init— explains that shell profile modifications are unnecessary; guides the user to addcondabinto 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, status, shell, help, or a disabled command), it replaces its own process with the installed conda binary using the Unix execvp syscall. For create and env create, cx runs conda as a subprocess to filter misleading conda activate hints from the output, replacing them with cx shell guidance. 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. 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
action.yml Composite GitHub Action for building custom cx binaries
Formula/cx.rb Homebrew formula (same-repo tap)
pixi.lock Locked dev dependencies
build.rs Compile-time solver and lockfile generator
cx.lock Cached rattler-lock v6 lockfile (checked in)
cx.lock.hash Config hash for cx.lock cache invalidation
CHANGELOG.md Release changelog
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
scripts/
get-cx.sh Installer script for macOS/Linux
get-cx.ps1 Installer script for Windows (PowerShell)
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 Symlink to ../CHANGELOG.md
guides/
custom-builds.md How to build custom cx binaries
reference/
cli.md CLI reference
github-action.md Composite action and reusable workflow reference
installer.md Installer script reference
.github/
workflows/
ci.yml CI: build, test, lint on all platforms (canary artifacts)
release.yml Build binaries + wheels, publish to GitHub Releases, PyPI, and crates.io
build.yml Reusable workflow for building custom cx binaries (workflow_call)
docs.yml Build and deploy Sphinx docs to GitHub Pages
Development environment#
cx uses pixi to manage the Rust toolchain from conda-forge, ensuring consistent builds across local development and CI:
# Install pixi (if not already installed)
curl -fsSL https://pixi.sh/install.sh | bash
# 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:
[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.
Build-time environment variable overrides#
For custom builds (e.g. via the reusable GitHub Action), build.rs supports environment variable overrides that replace the pixi.toml values:
Variable |
Overrides |
Format |
|---|---|---|
|
|
Comma-separated match specs |
|
|
Comma-separated channel names |
|
|
Comma-separated package names |
When overrides are active, the checked-in cx.lock is skipped (a fresh solve runs) and the repo-root lockfile is not overwritten.
CLI#
cx bootstrap [--force] [--prefix DIR] [--channel CH] [--package PKG]
[--exclude PKG] [--no-exclude] [--no-lock] [--lockfile PATH]
cx status [--prefix DIR]
cx shell [ENV] # alias for conda spawn (activate via subshell)
cx uninstall [--prefix DIR] [--yes] # remove prefix, envs, binary, PATH entries
cx <any-conda-command> # transparently delegates to conda
Default prefix: ~/.cx
Disabled commands#
Command |
Behavior |
|---|---|
|
Prints guidance to use |
|
Prints guidance to use |
|
Explains |
Default installed plugins#
Plugin |
Purpose |
|---|---|
conda-rattler-solver |
Rust-based solver (replaces libmamba) |
conda-spawn |
Subprocess-based activation (replaces |
conda-pypi |
PyPI interoperability (install, solve, convert) |
conda-self |
Base environment self-management |
Planned additions#
Plugin |
Purpose |
Blocker |
|---|---|---|
pixi-inspired task runner ( |
Needs conda-forge feedstock |
|
Multi-environment workspace management ( |
Needs conda-forge feedstock (dep: conda-lockfiles is already on conda-forge) |
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 ecosystem:
Crate |
Role |
|---|---|
|
Package installation engine |
|
SAT-based dependency solver |
|
Repodata fetching (sharded) |
|
conda type definitions |
|
Lockfile read/write (v6 format) |
|
Virtual package detection |
|
Auth middleware, OCI support |
|
Cache directory management |
PyPI distribution#
cx is published to PyPI as platform wheels via maturin (bindings = "bin"), following the same pattern as uv. A tiny Python wrapper in python/conda_express/ provides:
find_cx_bin()— locates the binary via sysconfigpython -m conda_express— finds and exec’s the cx binary
CI/CD#
All workflows use pixi for toolchain management:
ci.yml— runs on push tomainand PRs. Builds and tests across 5 targets (linux-x64, linux-aarch64, macos-x64, macos-arm64, windows-x64). Uploads canary binaries as artifacts. Runspixi run lintseparately.release.yml— triggers on tag push (v*). Orchestrates the full release pipeline: builds native binaries, builds maturin platform wheels and sdist, creates a GitHub Release with binary assets, publishes wheels to PyPI via trusted publishing (OIDC), and publishes the crate to crates.io via trusted publishing (rust-lang/crates-io-auth-action). All steps run as separate jobs with dependency ordering.build.yml— reusable workflow (workflow_call) for building custom cx binaries. Acceptspackages,channels,exclude, andrefinputs. Builds all 5 platforms using the composite action and uploads binary artifacts with checksums.docs.yml— triggers on push tomain(docs paths), PRs, and manual dispatch. Builds Sphinx documentation and deploys to GitHub Pages.
Composite action (action.yml)#
The repo root contains a composite GitHub Action that lets other repos build custom cx binaries with uses: jezdez/conda-express@main. It accepts packages, channels, exclude, and ref inputs, checks out conda-express, builds with env var overrides, and outputs the binary-path and asset-name. Callers handle their own platform matrix.
Future work#
See PLAN.md for the full roadmap, including the conda-self updater plugin design, homebrew-core submission, conda-forge feedstock, and upstream work.