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
One-shot commands¶
To execute a command without switching to a shell; or for scripting, use
Quirks¶
-
The Python environment is read-only
Development shells (devshells) are immutable, this extends to packages sourced from PyPI.
uvwill neither sync nor install packages outside the initially defined set. To modify packages, editpyproject.toml, then generate an updated lockfile by runninguv lockin 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
.devleverages this to have better control over dependencies instead of relying on platform-specific package sources. We do not do this in devshells, preferringnixpkgswhen feasible (and falling back on PyPI when it isn't, segmenting these packages as.libinpyproject.toml). This leads to predictable drift between the versions pinned inuv.lockand versions published in the pinned snapshot ofnixpkgsinflake.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
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
One-shot commands¶
To execute a command without switching to a shell; or for scripting, from the contrib/docker directory, run
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
Quirks¶
-
nix_shelldoesn't work standalonenix_shellintentionally does not host the daemon, instead, delegating that to a dedicatednix_daemoncontainer. 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.