Skip to content

Development Shells

Nix

To maintain a consistent development environment and reproducible toolchain, a declarative environment is available using Nix on macOS and Linux hosts on ARM64 and AMD64. Windows users are recommended to either resort to using Nix through Windows Subsystem for Linux or manually set up their environment.

Tip

Should you wish to utilize devshells without installing Nix on your host, the environment is also available wrapped in a Docker container; though it is still recommended to set up Nix for long-term development due to the cost associated with maintaining a parallel Nix store.

Setting up Nix

Warning

macOS 26 "Tahoe" is the last release supporting Intel-based Macs (source). Support for it as a host is on a best-effort basis while it is still an officially supported target platform. nixpkgs dropped x86_64-darwin per NixOS/nixpkgs#535508 (included in 26.11).

The environment is therefore pinned to its prior release, 26.05, and will stay there for as long as it remains reasonable.

For install guidance on Linux, see here. For macOS, while Nix is an option, Determinate Nix has been found to better accommodate macOS-specific quirks and guidance for that is available here. That being said, regardless of choice of Nix distribution used (including independent projects like Lix), nix-command and flakes features need to be enabled (guidance for enablement should be taken from your distribution vendor).

Entering a shell

To enter an interactive shell, from the repository root, use

nix develop ./contrib/nix#dev

One-shot commands

To execute a command without switching to a shell; or for scripting, use

nix develop ./contrib/nix#dev --command cargo test --workspace --features full

Quirks

  • The Python environment is read-only

    Development shells (devshells) are immutable, this extends to packages sourced from PyPI. uv will neither sync nor install packages outside the initially defined set. To modify packages, edit pyproject.toml, then generate an updated lockfile by running uv lock in the shell. Then re-enter a fresh devshell, it should take on the new definitions.

  • Pinned versions of tools don't match against manual setup

    Manual setup installs PyPI-sourced dependencies with .dev. Not every PyPI package is written in Python nor does it have to expose a Pythonic API to qualify for publication on PyPI. This allows PyPI to serve as a general means to distribute binaries so long as they otherwise meet PyPI's guidelines.

    This makes PyPI serve as a parallel package source and .dev leverages this to have better control over dependencies instead of relying on platform-specific package sources. We do not do this in devshells, preferring nixpkgs when feasible (and falling back on PyPI when it isn't, segmenting these packages as .lib in pyproject.toml). This leads to predictable drift between the versions pinned in uv.lock and versions published in the pinned snapshot of nixpkgs in flake.lock.

    This drift is benign. Should there be any difference in outcome, consider filing an issue.

Docker

Tip

On some platforms, docker compose is not included with the baseline Docker installation. In that case, you may need to consult platform-specific guidance on installing Compose, like the docker-compose package on Debian.

To install Docker on your host, see official guidance for your platform. Note that unlike using Nix, the store used in Docker cannot be shared with the host and using the provided containers is highly discouraged if you already use Nix on your host.

The Nix store is persisted as the volume nix_store and is expected to consume 15-20GB at a minimum, it is managed by the nix_daemon container and other Nix daemons must not compete for management of this store.

Warning

The workspace is bind-mounted and the Compose project is incompatible with worktrees. If you are using paired programming assistants like Claude Code or Codex, there is a fair chance worktrees are in use. Worktrees resolve their parent repository by absolute path on the host, which isn't visible from the vantage point of the container.

To build the image, from the contrib/docker directory, run

docker compose build

Entering a shell

Note

To prevent permissions issues, HOST_UID and HOST_GID are supplied to ensure that the container uses the same UID:GID pair as the source code it is bind-mounted against. If undefined, they default to the default Linux pair, 1000:1000.

To enter an interactive shell, from the contrib/docker directory, run

# Starts the containers and drops you into an interactive shell, reaped on exit. The daemon is left running
HOST_UID=$(id -u) HOST_GID=$(id -g) docker compose run --rm nix_shell

To shut down the daemon, from the contrib/docker directory, run

docker compose down

One-shot commands

To execute a command without switching to a shell; or for scripting, from the contrib/docker directory, run

docker compose run --rm nix_shell cargo build --workspace

Reaping the Nix store

To get rid of the persistent store and the build cache, freeing the associated space, from the contrib/docker directory, run

docker compose down --volumes

Quirks

  • nix_shell doesn't work standalone

    nix_shell intentionally does not host the daemon, instead, delegating that to a dedicated nix_daemon container. This is to avoid churn and to achieve better isolation. Nix stores are expensive in storage cost (and initially for built elements, compute), so the interactive container talks to the store-hosting container, permitting flexible setups where multiple containers can leverage the same underlying store.

    Managing the containers outside Docker Compose is unsupported.