Fast, Declarative, Reproducible, and Composable Developer Environments using Nix
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
When creating git commits, add entry to CHANGELOG.md (skip for src/modules/* and docs/* and refactorings not affecting behavior):
## Build & Development Commands
Use `devenv shell` to get a shell with Rust and all dependencies installed.
Prefix commands with `devenv shell --` to run them directly.
- **Build**: `cargo build`
- **Run CLI**: `cargo run -- [args]`
- **Build with Nix**: `nix build`
- **Format**: `cargo fmt`
- **Lint**: `cargo clippy`
- **Run all tests**: `devenv-run-tests run tests`
- **Run single test**: `devenv-run-tests run tests --only <test_name>`
- **Run unit tests**: `cargo nextest run`
- **Run unit tests (single crate)**: `cargo nextest run -p <crate_name>`
- **Run all unit tests (including feature-gated)**: `cargo nextest run --features devenv/test-all`
## Architecture Overview
devenv is a Rust CLI tool that creates fast, declarative, reproducible developer environments using Nix. The codebase is organized as a Cargo workspace.
### Core Crates
- **devenv/** - Main CLI binary. Entry point is `main.rs` which handles three runtime modes (TUI, legacy CLI, tracing). Command dispatch happens in `devenv.rs`. CLI definitions use clap in `cli.rs`.
- **devenv-core/** - Shared types and abstractions:
- `config.rs` - Configuration parsing (`devenv.yaml`, `devenv.local.yaml`)
- `evaluator.rs` - The `Evaluator` trait that abstracts Nix evaluation
- `store.rs` - The `Store` trait for talking to a Nix store
- `backend.rs` - `Backend<E>`, devenv-shaped facade over an `Evaluator`
- `paths.rs` - `DevenvPaths` on-disk layout
- **devenv-nix-backend/** - C FFI-based Nix backend using `nix-bindings-*` crates. Provides direct API access to Nix without subprocess spawning. This is the default backend.
- **devenv-snix-backend/** - Experimental pure-Rust Nix evaluator backend using Snix (feature-gated with `snix`).
- **devenv-tasks/** - DAG-based task execution system with caching, parallel execution, and privilege escalation support.
- **devenv-activity/** - Tracing-based activity system that powers the TUI progress display. Use `#[instrument_activity("description")]` macro for TUI-visible operations.
- **devenv-tui/** - Terminal UI for displaying build progress and activities.
- **devenv-eval-cache/** - SQLite-based caching for Nix evaluation results. Tracks file and env dependencies to invalidate cache.
- **devenv-cache-core/** - Shared utilities for file hashing and SQLite operations used by both eval and task caches.
- **devenv-run-tests/** - Test harness for integration tests. Runs tests in isolated temp directories with fresh environments.
- **tokio-shutdown/** - Graceful shutdown manager handling SIGINT/SIGTERM with cleanup coordination.
- **nix-conf-parser/** - Parser for `nix.conf` format (output of `nix config show`).
- **xtask/** - Build automation (manpage and shell completion generation).
### Configuration Flow
1. User creates `devenv.yaml` (inputs) and `devenv.nix` (configuration)
2. `Config::load()` in devenv-core parses YAML and resolves inputs
3. `Devenv::assemble()` generates a temporary flake structure in `.devenv/`
4. `NixBackend` evaluates the flake to produce shell environment or build outputs
### Key Patterns
- **Dual Backend Architecture**: The `NixBackend` trait allows swapping between the FFI-based backend (default) and Snix backend.
- **Activity Tracing**: Use `#[instrument_activity("description")]` macro or `activity!(INFO, operation, "...")` for TUI-visible operations.
- **Error Handling**: Use `miette` for errors with `bail!()` and `?`. Custom error types use `thiserror`.
- **SQLite Migrations**: Both `devenv-eval-cache` and `devenv-tasks` use sqlx with migrations in `migrations/` directories.
## Testing
Integration tests live in `tests/` and `examples/` directories. Each test is a directory containing:
- `devenv.nix` - The configuration to test
- `.test.sh` - Test script (runs inside devenv shell by default)
- `.test-config.yml` (optional) - Test configuration:
- `use_shell: false` - Run `.test.sh` directly, not in devenv shell
- `git_init: false` - Don't initialize git repo in temp dir
- `supported_systems` / `broken_systems` - Platform filtering
### Cargo Feature Flags
- **`devenv/test-all`** — Enables all feature-gated unit tests across the workspace.
Propagates to `devenv-processes/test-all`, `devenv-nix-backend/test-all`, `devenv-reload/test-all`, `devenv-shell/test-all`, and `devenv-tui/test-all`.
- **`devenv/test-mcp`** — Enables MCP-related tests.
- **`devenv/snix`** — Enables the experimental Snix backend.
- **`devenv-shell/test-pty`** — Enables PTY-based session tests (requires a real terminal).
- **`deterministic-tui`** — Available on both `devenv-shell` and `devenv-tui`.
Replaces spinner animation and elapsed time formatting with static placeholders (`[TIME]`, fixed spinner frame) so that TUI snapshot tests produce deterministic output.
Enabled automatically by `test-all` on both crates.
## Tracing / Debugging
Tracing is disabled by default.
Enable it with `--trace-to` using the syntax `[format:]destination`.
Multiple outputs can be active simultaneously by repeating the flag.
- **Trace to stderr**: `cargo run -- --trace-to stderr shell`
- **Trace to file**: `cargo run -- --trace-to file:/tmp/devenv.log shell`
- **Pretty format**: `cargo run -- --trace-to pretty:stderr shell`
- **JSON format** (default): `cargo run -- --trace-to json:stderr shell`
- **Full format**: `cargo run -- --trace-to full:stderr shell`
- **Multiple outputs**: `cargo run -- --trace-to pretty:stderr --trace-to json:file:/tmp/trace.json shell`
When format is omitted, defaults to json.
When any output targets stdout or stderr, the TUI is automatically disabled.
Environment variable `DEVENV_TRACE_TO` accepts comma-separated specs (e.g. `DEVENV_TRACE_TO=pretty:stderr,json:file:/tmp/t.json`).
### Authoring trace events
`tracing` (`trace!`, `debug!`, `info!`, `warn!`, `error!`) is for **developers** debugging devenv itself — visible via `--trace-to` or `RUST_LOG`.
**User-facing output is the activity system**, not `tracing`.
Use `activity!(...)`, `instrument_activity`, or `message(level, "...")` to surface anything the user should see in the TUI or non-TUI console.
Level choice:
- `error!` — operation failed, user needs to know.
- `warn!` — degraded path that succeeded (fallback used, retry happened).
- `info!` — major lifecycle events worth noting in a normal run.
- `debug!` — diagnostics useful when something goes wrong (cache hits/misses, decision points, request boundaries). Enabled by `--verbose`. Cost: small.
- `trace!` — fine-grained internal flow (per-iteration, per-byte, per-state-transition). Enabled only by explicit `RUST_LOG=trace`. Cost: can be hot.
Rule of thumb: if an operator debugging a production issue would want it → `debug!`. If only the library author tracing internal flow would want it → `trace!`.
Message style: lowercase, no leading capital, no trailing period. Structured fields carry the data. Examples:
```rust
debug!(key_hash = %key, "cache hit");
trace!(error = %e, "task join error");
warn!(path = %p.display(), "failed to read file, falling back to default");
```
## Code Style
- **Error Handling**: Use `bail!()` not `panic!()`, propagate with `?`
- **No unsafe**: Don't use `unsafe` code
## Docs
When adding documentation in `docs/`, make sure to note the version the change was added in by checking `Cargo.toml`.
!!! tip "New in version X.Y.Y"
## Changelog
`CHANGELOG.md` follows this structure:
```
## <version> (unreleased)
### Bug Fixes
- Fixed <description> ([#<issue>](https://github.com/cachix/devenv/issues/<issue>)).
### Improvements
- <Description of improvement>.
### Breaking Changes
- **<Name>**: <Description>.
```
When updating the changelog:
1. Use `git log <last-release-commit>..HEAD` to find commits since the last release.
2. Skip automated commits (e.g. `Auto generate ...`) and test-only commits.
3. Group entries under **Bug Fixes**, **Improvements**, or **Breaking Changes**.
4. Link GitHub issues when referenced in commit messages (search for `Fixes`/`Closes`/`#` in commit bodies).
5. The unreleased section sits above the last release entry.
6. Write for users, not maintainers: describe the symptom and user-visible outcome. Omit implementation details (internal variable names, struct names, code paths). One or two sentences max.
## Files That Should Not Be Edited
- `docs/reference/options.md` - Auto-generated from Nix module options