conda-express (cx): Implementation Plan#
For architecture, current features, and file structure see DESIGN.md.
For ecosystem context, prior art, and rationale see the Background & rationale docs page.
Remaining work#
Phase 1: Ecosystem integration#
Task |
Status |
Issue |
|---|---|---|
Include conda-workspaces in default package set |
Blocked (needs conda-forge feedstock) |
|
Submit cx-wasm packages to emscripten-forge |
Not started |
|
PyPI wheel support (repodata v3 / conda-pypi) |
Not started |
|
conda-forge feedstock for cx |
Not started |
|
conda-self pluggable updater backend |
Not started |
|
Homebrew-core submission |
Not started (needs adoption) |
|
Offline bootstrap from bundled payload (PKG / MSI / local channel) |
Done |
Phase 2: Production polish#
Task |
Status |
Issue |
|---|---|---|
Explore crate name transfer for |
Not started |
Upstream work (nice to have) (#5)#
These improve conda’s ecosystem health but are not required for cx:
Make conda-libmamba-solver optional on conda-forge — would eliminate cx’s post-solve exclusion hack and make
conda self updatework without a custom backendPR to conda/conda: make pycosat and menuinst optional (revive jaimergp’s PR #14170 approach)
Publish conda-classic-solver to defaults and conda-forge (unblocks solver extraction)
Publish conda-rattler-solver, conda-spawn, conda-self to PyPI
Publish conda itself to PyPI (reclaim the yanked
condapackage name)Publish conda-spawn to Anaconda defaults — last cx package not on defaults; would unblock a defaults-only cx configuration
Completed work#
Phase 0: cx Rust prototype#
All core functionality implemented and tested. See DESIGN.md for the full feature table and architecture.
Phase 1: Ecosystem integration (completed items)#
Task |
Status |
|---|---|
Publish cx to PyPI via maturin |
Done |
Publish cx to crates.io |
Done |
Reusable GitHub Action (composite action + workflow) |
Done |
Build-time env var overrides ( |
Done |
|
Done |
Homebrew formula (same-repo tap) |
Done |
Installer scripts (get-cx.sh, get-cx.ps1) |
Done |
cx-wasm crate (browser solver + extractor) |
Done |
Async shard prefetch (two-phase fetch/solve) |
Done |
conda-emscripten plugin (solver, extractor, magics, patches) |
Done |
cx-jupyterlite extension (conda command interception) |
Done |
Shared library loading for C extensions |
Done |
MEMFS compatibility patches (download, extract, subprocess) |
Done |
JupyterLite demo + GitHub Pages deployment |
Done |
Documentation (Sphinx, Diataxis structure) |
Done |
Design proposals#
conda-self pluggable updater backend (#4)#
cx intentionally does not implement its own update command. conda self update is the canonical way to update conda across all installation methods (miniconda, miniforge, cx, future pip-installed conda). This requires conda-self to support pluggable updater backends.
Why cx can’t use conda-self’s default backend#
conda-self currently shells out to conda install --prefix=sys.prefix conda. This works in miniconda/miniforge, but in a cx-managed prefix it would re-introduce conda-libmamba-solver — conda on conda-forge hard-depends on it, and the solver has no way to exclude a required dependency. cx’s post-solve filtering only works at the rattler level, outside conda’s own solver.
Additionally, the base prefix is frozen via CEP 22. conda-self must override the frozen check when updating.
Proposed design#
conda-self should define a new plugin hook via conda’s existing pluggy system:
class CondaSelfUpdaterSpec:
@plugins.hookspec
def conda_self_updaters(self) -> Iterable[plugins.CondaSelfUpdater]:
"""Register a self-update backend."""
@dataclass
class CondaSelfUpdater:
name: str # e.g., "conda", "pip", "cx"
check_updates: Callable # check what updates are available
install_updates: Callable # perform the update
priority: int = 0 # higher priority wins if multiple registered
Backend implementations#
conda backend (default, current behavior):
check_updates: query conda channels for newer versionsinstall_updates:conda install --prefix=sys.prefix --override-frozen-envShips with conda-self itself
cx backend (for rattler-bootstrapped installs):
Detects cx-managed prefix via
.cx.jsonmarker filecheck_updates: two-level check — conda-forge for newer packages, GitHub releases (or PyPI) for a newer cx binaryinstall_updates: shells out tocx _internal-update(hidden subcommand) which uses rattler to re-solve with exclusion logicShips as a small Python package installed into the cx prefix
pypi backend (for pip/uv-installed conda):
check_updates: query PyPI for newer versionsinstall_updates: ideally uses conda-pypi for consistency; falls back to pip/uv
User experience#
conda self update # works regardless of installation method
cx self update # cx execs to conda, which runs conda-self
Detection logic#
Each backend declares a detect() method:
cx backend: checks for
.cx.jsoninsys.prefixconda backend: checks for
conda-meta/conda-*.json(default fallback)pypi backend: checks
importlib.metadatafor PyPI installer origin
Workaround until the plugin exists#
cx bootstrap --force
PyPI wheel support in cx-wasm (#3)#
Enable conda install <package> in the browser to install pure Python wheels from PyPI via the conda-pypi plugin and repodata v3 format.
conda-pypi-test channel#
URL:
https://github.com/conda-incubator/conda-pypi-test/releases/downloadIndexes ~500K pure Python wheels (
-none-any.whlonly) from PyPINot sharded — plain
repodata.json(+.zstcompressed variant)Uses the grayskull PyPI-to-conda mapping for name normalization
Repodata v3 format#
Wheel entries use a v3.whl top-level key, defined by three draft CEPs:
CEP 111: Conditional dependencies, extras, and flags
CEP 145: Repodata wheel support
CEP 146: Backwards-compatible repodata update strategy
Implementation timeline:
Date |
Component |
Change |
|---|---|---|
Feb 26, 2026 |
|
Added |
Mar 9, 2026 |
py-rattler v0.23.0 |
Exposes v3 support to Python (PR #2007) |
Mar 17, 2026 |
conda-pypi PR #273 |
Merged. v3 repodata with |
Pending |
conda-pypi-test PR #19 |
Blocked on conda-pypi release (#277) |
conda-pypi plugin hooks#
Hook |
Function |
WASM-compatible |
|---|---|---|
|
|
Yes |
|
|
No |
|
Post-install hooks (subprocess) |
No |
Only the extractor hook is needed for WASM.
cx-wasm gaps to bridge#
Repodata parsing:
parse_repodata_textinsharded.rsignoresexperimental_v3— needs to chainv3.whlrecordsAbsolute download URLs:
WhlPackageRecordentries point tofiles.pythonhosted.org, not channel-relative pathsRepodata size: ~500K packages in one
repodata.json— need.zstcompression or subsettingWheel extraction: conda-pypi extractor and
installerlibrary must be available in JupyterLiteDefensive patching: conda-pypi’s
post_commandhook calls subprocess — needs targeted disabling in WASM
emscripten-forge publishing (#2)#
Package |
Type |
Notes |
|---|---|---|
|
noarch |
Patched 26.1.1 with emscripten patches |
|
noarch |
Solver + extractor + vpkgs + magic plugin |
|
noarch |
WASM files + Python bridge |
|
noarch |
2.4.6 pure Python (may already exist on emscripten-forge) |
Once published, lite/environment.yml can reference these as dependencies, eliminating --with-local builds and simplifying GitHub Pages CI.
Offline bootstrap from bundled payload (PKG / MSI) (#11)#
Native installers (macOS PKG, Windows MSI) can ship cx together with a pre-downloaded set of conda packages (and lockfile / repodata as needed). Bootstrap would install from local paths only—no network on first run—reusing the same embedded rattler lockfile and Installer path as today.
This targets air-gapped or policy-restricted environments and vendors who want signed PKG/MSI plus a reproducible payload while keeping one cross-platform bootstrapper. Design work: CLI flags or env vars for a local channel root or package cache, documented packager workflow, and CI examples. See the issue for full scope.
Open risks#
Requires network on first run: Addressed by
--payload/--offlineflags (#11); packaging workflows can bundle archives for air-gapped bootstrap.conda-self hook design: Needs buy-in from conda-self maintainers (#4).
conda-index dependency: conda-pypi depends on conda-index — needs PyPI availability verification.
menuinst on Windows:
initialize.pyimportsmenuinst.knownfolders/menuinst.winshortcutbehindif on_win:— needs a try/except guard (upstream).