Build a custom cx binary#
This guide shows how to build a cx binary with your own set of conda packages baked in. This is useful when you want to distribute a bootstrapper that includes domain-specific packages (e.g. numpy, pandas) out of the box.
Using the GitHub Action#
The simplest approach. Add a workflow to your repo that calls the cx composite action:
name: Build custom cx
on: [push]
jobs:
build:
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: jezdez/conda-express@main
id: cx
with:
packages: "python >=3.12, conda >=25.1, conda-rattler-solver, conda-spawn, numpy, pandas"
- uses: actions/upload-artifact@v4
with:
name: ${{ steps.cx.outputs.asset-name }}
path: ${{ steps.cx.outputs.binary-path }}
The action builds cx for the runner’s platform and outputs the path to the binary. Use a matrix to build for multiple platforms.
See the GitHub Action reference for all inputs and outputs.
Using the reusable workflow#
If you want all 5 platforms built without managing a matrix yourself:
name: Build custom cx
on: [push]
jobs:
build-cx:
uses: jezdez/conda-express/.github/workflows/build.yml@main
with:
packages: "python >=3.12, conda >=25.1, conda-rattler-solver, conda-spawn, numpy, pandas"
channels: "conda-forge"
exclude: "conda-libmamba-solver"
Binary artifacts for all platforms are uploaded automatically.
Building locally#
Set environment variables to override the default package list, then build:
git clone https://github.com/jezdez/conda-express.git
cd conda-express
CX_PACKAGES="python >=3.12, conda >=25.1, conda-rattler-solver, conda-spawn, numpy" \
pixi run build
The binary is at target/release/cx.
Available overrides#
Variable |
Effect |
|---|---|
|
Replace the default package list |
|
Replace the default channels |
|
Replace the default exclusions |
Empty values are ignored. See the configuration reference for details on how overrides interact with the lockfile cache.
Choosing packages#
When specifying packages, keep in mind:
Always include the core set:
python,conda,conda-rattler-solver, andconda-spawn(cx depends on these at runtime)Use MatchSpec syntax for version constraints (e.g.
numpy >=1.26)The build performs a full dependency solve at compile time, so all transitive dependencies are resolved and locked
The resulting binary is self-contained with an embedded lockfile
Bundling a payload for offline bootstrap#
A custom cx binary can be paired with a pre-downloaded set of package archives for fully offline, air-gapped installation. This is useful for native installers (macOS PKG, Windows MSI) and restricted-network deployments.
Preparing the payload#
Run an online bootstrap once to populate the rattler package cache, then copy the archives into a payload directory:
# Bootstrap online to populate the cache
cx bootstrap --prefix /tmp/seed
# Collect the cached archives
mkdir payload
cp ~/Library/Caches/rattler/cache/pkgs/*.conda payload/
cp ~/Library/Caches/rattler/cache/pkgs/*.tar.bz2 payload/ 2>/dev/null || true
Note
The rattler cache location varies by platform: ~/Library/Caches/rattler
on macOS, ~/.cache/rattler on Linux, and %LOCALAPPDATA%\rattler on
Windows.
Using the payload#
Bundle the cx binary and the payload directory in your installer. In the post-install script, bootstrap from the payload:
cx bootstrap --payload /path/to/payload --offline
Or using environment variables (convenient for installer scripts):
CX_PAYLOAD=/path/to/payload CX_OFFLINE=1 cx bootstrap
Payload with network fallback#
If you want the payload to cover most packages but allow network fallback for
any missing ones, omit --offline:
cx bootstrap --payload /path/to/payload
This pre-populates the cache from the payload, then downloads anything not found locally.
Building cxz (self-contained binary)#
cxz is a variant of cx that embeds all package archives directly into the
binary. The result is a single 50-95 MB file (varies by platform) that bootstraps conda with zero
network access — drop it on any machine and run cxz bootstrap.
Building locally#
CX_EMBED_PAYLOAD=1 pixi run build
cp target/release/cx cxz
The build downloads all locked packages and bundles them as a zstd-compressed
tar archive inside the binary. This composes with CX_PACKAGES, CX_CHANNELS,
and CX_EXCLUDE — you can build a custom package set and embed it in one step:
CX_PACKAGES="python >=3.12, conda, numpy" CX_EMBED_PAYLOAD=1 pixi run build
Building via the GitHub Action#
- uses: jezdez/conda-express@main
with:
packages: "python >=3.12, conda, conda-rattler-solver, conda-spawn"
embed-payload: "true"
The action produces a cxz-<target> artifact instead of cx-<target>.
Building via the reusable workflow#
jobs:
build-cxz:
uses: jezdez/conda-express/.github/workflows/build.yml@main
with:
packages: "python >=3.12, conda, conda-rattler-solver, conda-spawn"
embed-payload: "true"
Using cxz#
./cxz bootstrap
No --payload, no --offline, no environment variables needed. cxz detects
its embedded payload automatically and uses it. All other cx flags and
subcommands work identically.
Explicit --payload still takes priority over the embedded payload, so you can
override the built-in packages at runtime if needed.