devenv devlog: Processes are now tasks

Building on the task runner, devenv now exposes all processes as tasks named devenv:processes:<name>.

Now you can run tasks before or after a process runs - addressing a frequently requested feature for orchestrating the startup sequence.

Usage

Execute setup tasks before the process starts

devenv.nix
{
  processes.backend = {
    exec = "cargo run --release";
  };

  tasks."db:migrate" = {
    exec = "diesel migration run";
    before = [ "devenv:processes:backend" ];
  };
}

When you run devenv up or the individual process task, migrations run first.

Run cleanup after the process stops

devenv.nix
{
  processes.app = {
    exec = "node server.js";
  };

  tasks."app:cleanup" = {
    exec = ''
      rm -f ./server.pid
      rm -rf ./tmp/*
    '';
    after = [ "devenv:processes:app" ];
  };
}

Implementation

Under the hood, process-compose now runs processes through devenv-tasks run --mode all devenv:processes:<name> instead of executing them directly. This preserves all existing process functionality while adding task capabilities.

The --mode all flag ensures that both before and after tasks are executed, maintaining the expected lifecycle behavior.

What's next?

Future work on process dependencies (#2037) will also address native health check support (process-compose#371), eliminating the need for manual polling scripts.

Domen

devenv 1.8: Progress TUI, SecretSpec Integration, Listing Tasks, and Smaller Containers

devenv 1.8 fixes a couple of annoying regressions since the 1.7 release, but also includes several new features:

Progress TUI

We've rewritten our tracing integration to improve reporting on what devenv is doing.

More importantly, devenv is now fully asynchronous under the hood, enabling parallel execution of operations. This means faster performance in scenarios where multiple independent tasks can run simultaneously.

The new progress interface provides real-time feedback on what devenv is doing:

devenv progress bar

We're continuing to improve visibility into Nix operations to give you even better insights into the build process.

SecretSpec Integration

We've integrated SecretSpec, a new standard for declarative secrets management that separates secret declaration from provisioning.

This allows teams to define what secrets applications need while letting each developer, CI system, and production environment provide them from their preferred secure provider.

Learn more in Announcing SecretSpec Declarative Secrets Management.

Task improvements

Listing tasks

The devenv tasks list command now groups tasks by namespace, providing a cleaner and more organized view:

$ devenv tasks list
backend:
  └── lint (has status check)
      └── test
          └── build (watches: src/backend/**/*.py)
deploy:
  └── production
docs:
  └── generate (watches: docs/**/*.md)
      └── publish
frontend:
  └── lint
      └── test (has status check)
          └── build

Running multi-level tasks

You can now run tasks at any level in the hierarchy. By default, tasks run in single mode (only the specified task):

# Run only frontend:build (default single mode)
$ devenv tasks run frontend:build
Running tasks     frontend:build
Succeeded         frontend:build                           5ms
1 Succeeded                         5.75ms

# Run frontend:build with all its dependencies (before mode)
$ devenv tasks run frontend:build --mode before
Running tasks     frontend:build
Succeeded         frontend:lint                            4ms
Succeeded         frontend:test                            10ms
Succeeded         frontend:build                           4ms
3 Succeeded                         20.36ms

# Run frontend:build and all tasks that depend on it (after mode)
$ devenv tasks run frontend:build --mode after
Running tasks     frontend:build
Succeeded         frontend:build                           5ms
Succeeded         deploy:production                        5ms
2 Succeeded                         11.44ms

CLI improvements

Package options support

The CLI now supports specifying single packages via the --option flag (#1988). This allows for more flexible package configuration directly from the command line:

$ devenv shell --option "languages.java.jdk.package:pkg" "graalvm-oracle"

Container optimizations

The CI container ghcr.io/cachix/devenv/devenv:v1.8 has been reduced (uncompressed) from 1,278 MB in v1.7 to 414 MB in v1.8—that's a reduction of over 860 MB (67% smaller!).

This makes devenv container much faster to pull and more efficient in CI/CD pipelines.

Thank You

Join our Discord community to share your experiences and help shape devenv's future!

Domen

Announcing SecretSpec: Declarative Secrets Management

We've supported .env integration for managing secrets, but it has several issues:

  • Apps are disconnected from their secrets - applications lack a clear contract about which secrets they need
  • Parsing .env is unclear - comments, multiline values, and special characters all have ambiguous behavior across different parsers
  • Password manager integration is difficult - requiring manual copy-paste or template workarounds
  • Vendor lock-in - applications use custom parsing logic, making it hard to switch providers
  • No encryption - .env files are stored as plain text, vulnerable to accidental commits or unauthorized access

While we could recommend solutions like dotenvx to encrypt .env files or sops for general secret encryption, these bring new challenges:

  • Single key management - requires distributing and managing a master key
  • Trust requirements - everyone with the key can decrypt all secrets
  • Rotation complexity - departing team members require key rotation and re-encrypting all secrets

Larger teams often adopt solutions like OpenBao (the open source fork of HashiCorp Vault), requiring significant infrastructure and operational overhead. Smaller teams face a gap between simple .env files and complex enterprise solutions.

What if instead of choosing one tool, we declared secrets uniformly and let each environment use its best provider?

The Hidden Problem: Conflating Three Concerns

We've created SecretSpec and integrated it into devenv. SecretSpec separates secret management into three distinct concerns:

  • WHAT - Which secrets does your application need? (DATABASE_URL, API_KEY)
  • HOW - Requirements (required vs optional, defaults, validation, environment)
  • WHERE - Where are these secrets stored? (environment variables, Vault, AWS Secrets Manager)

By separating these concerns, your application declares what secrets it needs in a simple TOML file. Each developer, CI system, and production environment can provide those secrets from their preferred secure storage - without changing any application code.

One Spec, Multiple Environments, Different Providers

Imagine you commit a secretspec.toml file that declares:

# secretspec.toml - committed to your repo
[project]
name = "my-app"
revision = "1.0"

[profiles.default]
DATABASE_URL = { description = "PostgreSQL connection string", required = true }
REDIS_URL = { description = "Redis connection string", required = false }
STRIPE_API_KEY = { description = "Stripe API key", required = true }

[profiles.development]
# Inherits from default profile - only override what changes
DATABASE_URL = { default = "postgresql://localhost/myapp_dev" }
REDIS_URL = { default = "redis://localhost:6379" }
STRIPE_API_KEY = { description = "Stripe API key (test mode)" }

[profiles.production]
# Production keeps strict requirements from default profile
Now, here's the magic:

  • You (on macOS): Store it in Keychain, retrieve with secretspec --provider keyring run -- cmd args
  • Your teammate (on Linux): Store it in GNOME Keyring, same command works
  • That one developer: Still uses a .env file locally (we don't judge, we've been there)
  • CI/CD: Reads from environment variables in GitHub Actions secretspec --provider env run -- cmd args
  • Production: Secrets get provisioned using AWS Secret Manager

Same specification. Different providers. Zero code changes.

Example: One Spec, Three Environments

Let's walk through migrating from .env to SecretSpec.

Setting up secretspec for development

First, choose your default provider and profile:

$ secretspec config init
? Select your preferred provider backend:
> keyring: Uses system keychain (Recommended)
  onepassword: OnePassword password manager
  dotenv: Traditional .env files
  env: Read-only environment variables
  lastpass: LastPass password manager
? Select your default profile:
> development
  default
  none
✓ Configuration saved to ~/.config/secretspec/config.toml

Importing secrets

Create secretspec.toml from your existing .env:

$ secretspec init --from dotenv

1. Local Development with devenv (You're on macOS)

Enable SecretSpec in devenv.yaml:

secretspec:
  enable: true

In devenv.nix:

{ pkgs, lib, config, ... }:

{
  languages.rust.enable = true;

  services.minio = {
    enable = true;
    buckets = [ config.secretspec.secrets.BUCKET_NAME ];
  };
}

Start the minio process:

$ devenv up
✓ Starting minio...

2. CI/CD (GitHub Actions)

# .github/workflows/test.yml
- name: Run tests
  env:
    DATABASE_URL: {{ secrets.TEST_DATABASE_URL }}
    STRIPE_API_KEY: {{ secrets.STRIPE_TEST_KEY }}
  run: |
    secretspec run --provider env --profile production -- npm test

3. Production (Fly.io)

# fly.toml
[processes]
web = "secretspec run --provider env --profile production -- npm start"

# Set secrets using fly CLI:
# fly secrets set DATABASE_URL=postgresql://... STRIPE_API_KEY=sk_live_...
# SecretSpec will read these from environment variables

Notice what didn't change? Your secretspec.toml. Same specification, different providers, zero code changes.

Loading secrets in your application

While secretspec run provides secrets as environment variables, your application remains disconnected from knowing which secrets it requires. The Rust SDK bridges this gap by providing type-safe access to your declared secrets.

The Rust SDK provides compile-time guarantees:

// Generate typed structs from secretspec.toml
secretspec_derive::declare_secrets!("secretspec.toml");

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Load secrets using the builder pattern
    let secretspec = SecretSpec::builder()
        .with_provider("keyring")  // Can use provider name or URI like "dotenv:/path/to/.env"
        .with_profile(Profile::Production)  // Can use string or Profile enum
        .load()?;

    // Access secrets (field names are lowercased)
    println!("Database: {}", secretspec.secrets.database_url);  // DATABASE_URL → database_url
    println!("Stripe: {}", secretspec.secrets.stripe_api_key);  // STRIPE_API_KEY → stripe_api_key

    // Optional secrets are Option<String>
    if let Some(redis) = &secretspec.secrets.redis_url {
        println!("Redis: {}", redis);
    }

    // Access profile and provider information
    println!("Using profile: {}", secretspec.profile);
    println!("Using provider: {}", secretspec.provider);

    // For backwards compatibility, export as environment variables
    secretspec.secrets.set_as_env_vars();

    Ok(())
}

Add to your Cargo.toml:

[dependencies]
secretspec = "0.2.0"
secretspec_derive = "0.2.0"

The application code never specifies where to get secrets - only what it needs through the TOML file. This keeps your application logic clean and portable.

Building SDKs for Other Languages

We'd love to see more SDKs that bring this same declarative approach to Python, JavaScript, Go, and other languages.

A world of possibilities

We're exploring features for future workflows:

Final words

Let's make secret management as declarative as package management. Let's stop sharing .env files over Slack. Let's build better tools for developers.

Share your thoughts on our Discord community or open an issue on GitHub. We'd love to hear how you handle secrets in your team.

Domen

devenv 1.7: CUDA Support, Enhanced Tasks, and MCP support

devenv 1.7 brings several practical improvements:

Progress on Snix Support

We've started work on supporting multiple Nix implementations in devenv. The codebase now includes a backend abstraction layer that will allow users to choose between different Nix implementations.

This architectural change paves the way for integrating Snix (our development fork). While the Snix backend isn't functional yet, the groundwork is in place for building out this Rust-based reimplementation to the C++ Nix implementation. See PR #1950 for implementation details.

Platform-Specific Configuration

Here's how to enable CUDA support only on Linux systems while keeping your environment working smoothly on macOS:

  • CUDA-enabled packages are built with GPU support on Linux
  • macOS developers can still work on the same project without CUDA
  • The correct CUDA capabilities are set for your target GPUs
# devenv.yaml
nixpkgs:
  config:
    allowUnfree: true
    x86_64-linux:
      cudaSupport: true
      cudaCapabilities: ["7.5" "8.6" "8.9"]

Tasks Enhancements

Tasks now skip execution when their input files haven't changed, using the new execIfModified option:

{
  tasks = {
    "frontend:build" = {
      exec = "npm run build";
      execIfModified = [ "src/**/*.tsx" "src/**/*.css" "package.json" ];
    };

    "backend:compile" = {
      exec = "cargo build --release";
      execIfModified = [ "src/**/*.rs" "Cargo.toml" "Cargo.lock" ];
    };
  };
}

This dramatically speeds up incremental builds by skipping unnecessary work.

Namespace-Based Task Execution

Run all tasks within a namespace using prefix matching:

# Run all frontend tasks
$ devenv tasks run frontend

Model Context Protocol (MCP) Support

devenv now includes a built-in MCP server that enables AI assistants like Claude to better understand and generate devenv configurations:

# Start the MCP server
$ devenv mcp

AI assistants can now:

  • Search for packages and their options
  • Understand devenv's configuration format
  • Generate valid configurations based on your requirements

Quality of Life Improvements

  • Shell Integration: Your shell aliases and functions now work correctly
  • Clean Mode: Fixed shell corruption when using --clean
  • Error Messages: More helpful error messages when commands fail
  • State Handling: Automatically recovers from corrupted cache files
  • Direnv Integration: Fewer unnecessary environment reloads

Upcoming 1.8 Release

Standardized Language Tooling Configuration

All language modules will support the same configuration pattern (PR #1974):

{
  languages.rust.dev = {
    lsp.enable = false;
    debugger.enable = false;
    linter.enable = false;
    formatter.enable = false;
  };
}

Rust Import Functionality

Import Rust projects and their dependencies as Nix packages with the new languages.rust.import configuration (PR #1946):

{
  languages.rust.enable = true;
  languages.rust.import = {
    mypackage = {
      root = ./.;
    };
  };
  packages = languages.rust.import.mypackage.packages;
}

That allows us to bridge the gap between developer environments and fully packaged Rust applications using Nix.

Async Core

Operations that can run in parallel will (PR #1970).

Getting Started

Join our Discord community to share your experiences and help shape devenv's future.

We're particularly interested in feedback on the standardized language tooling configuration coming in 1.8 - let us know if this approach works for your use cases!

Domen

devenv 1.6: Extensible Ad-Hoc Nix Environments

devenv 1.6 has been tagged, allowing you to:

  • Create temporary environments directly from the command line without requiring a devenv.nix file.
  • Temporarily modify existing environments.

Create Environments on the Fly

Developer environments on demand using the new --option (-O) flag:

$ devenv --option languages.python.enable:bool true \
         --option packages:pkgs "ncdu git ripgrep" \
         shell

This command creates a temporary Python environment without writing any configuration files.

Ad-hoc environments are ideal for quickly testing languages or tools without committing to a full project setup:

$ devenv -O languages.elixir.enable:bool true shell iex

Supported Option Types

The --option flag supports multiple data types, making it flexible for various use cases:

  • :string for text values
  • :int for integers
  • :float for decimal numbers
  • :bool for true/false values
  • :path for file paths
  • :pkgs for specifying Nix packages

GitHub Actions with Matrices

One of the most powerful applications of ad-hoc environments is in CI pipelines, where you can easily implement testing matrices across different configurations:

jobs:
  test:
    strategy:
      matrix:
        python-version: ['3.9', '3.10', '3.11']
    steps:
      - uses: actions/checkout@v3
      - uses: cachix/install-nix-action@v31
      - uses: cachix/cachix-action@v16
        with:
          name: devenv
      - name: Install devenv.sh
        run: nix profile install nixpkgs#devenv
      - name: Test with Python {{ '${{ matrix.python-version }}' }}
        run: |
          devenv --option languages.python.enable:bool true \
                 --option languages.python.version:string {{ '${{ matrix.python-version }}' }} \
                 test

This approach lets you validate your code across multiple language versions or dependency combinations without maintaining separate configuration files for each scenario.

Combining with Existing Configurations

When used with an existing devenv.nix file, --option values override the configuration settings in the file, making it easy to temporarily modify your environment.

Switching Between Environment Profiles

Ad-hoc options are perfect for switching between predefined profiles in your development environment:

$ devenv --option profile:string backend up

This enables you to switch between frontend, backend, or other custom profiles without modifying your configuration files.

See our Profiles guide for more details on setting up and using profiles.

For complete documentation on this feature, visit our Ad-hoc Developer Environments guide.

We're excited to see how you'll use ad-hoc environments to streamline your development workflow. Share your feedback on GitHub or join our Discord community!

devenv 1.5: Overlays Support and Performance Improvements

In this release, we're introducing a powerful Nix concept: overlays for modifying and extending the nixpkgs package set, along with significant performance and TLS certificate improvements.

Overlays: Customizing Your Package Set

Overlays allow you to modify or extend the default package set (pkgs) that devenv uses. This is particularly useful when you need to:

  • Apply patches to existing packages
  • Use different versions of packages than what's provided by default
  • Add custom packages not available in nixpkgs
  • Use packages from older nixpkgs versions

Here's an example of using overlays in your devenv.nix file to apply a patch to the hello package:

{ pkgs, ... }:

{
  # Define overlays to modify the package set
  overlays = [
    # Override an existing package with a patch
    (final: prev: {
      hello = prev.hello.overrideAttrs (oldAttrs: {
        patches = (oldAttrs.patches or []) ++ [ ./hello-fix.patch ];
      });
    })
  ];

  # Use the modified packages
  packages = [ pkgs.hello pkgs.my-tool ];
}

Using packages from a different nixpkgs version

You can even use packages from a different nixpkgs version by adding an extra input to your devenv.yaml:

inputs:
  nixpkgs:
    url: github:cachix/devenv-nixpkgs/rolling
  nixpkgs-unstable:
    url: github:nixos/nixpkgs/nixpkgs-unstable

And then using it in your devenv.nix:

{ pkgs, inputs, ... }:

{
  overlays = [
    (final: prev: {
      nodejs = (import inputs.nixpkgs-unstable {
        system = prev.stdenv.system;
      }).nodejs;
    })
  ];

  # Now you can use the unstable version of Node.js
  languages.javascript.enable = true;
}

For more details and examples, check out the overlays documentation.

TLS Improvements: Native System Certificates

We've heard from ZScaler how they are using devenv and we've fixed their major annoyance by ensuring devenv now respects system certificates that many enterprises rely on.

macOS Development Enhancements: Custom Apple SDK Support

For macOS developers, we've added the ability to customize which Apple SDK is used for development:

{ pkgs, ... }:

{
  apple.sdk = pkgs.apple-sdk_15;
}

This allows you to: - Control exactly which version of the SDK to use - Ensure consistency across development environments - Avoid incompatibilities between different macOS versions

Performance Improvements

Sander further tweaked the performance of developer environment activation at OceanSprint when it can be cached:

  • Linux: ~500ms -> ~150ms
  • macOS: ~1300ms -> ~300ms

Join our Discord to share feedback and suggestions!

Domen

devenv 1.4: Generating Nix Developer Environments Using AI

One of the main obstacles in using Nix for development environments is mastering the language itself. It takes time to become proficient writing Nix.

How about using AI to generate it instead:

$ devenv generate a Python project using Torch
• Generating devenv.nix and devenv.yaml, this should take about a minute ...

You can also use devenv.new to generate a new environment.

Generating devenv.nix for an existing project

You can also tell devenv to create a scaffold based on your existing git source code:

$ devenv generate
• Generating devenv.nix and devenv.yaml, this should take about a minute ...

Telemetry

To continually enhance the AI’s recommendations, we collect anonymous data on the environments generated. This feedback helps us train better models and improve accuracy.

Of course, your privacy matters—if you prefer not to participate, just add the --disable-telemetry flag when generating environments. We also adhere to the donottrack standard.

Domen

devenv is switching its Nix implementation to Tvix

In February 2020, I went on a 16-day, 1200km moped trip across northern Thailand with a couple of friends.

Somewhere in the northern Thailand
Somewhere in northern Thailand near Pai.

As we drove for hours on end, I was listening to an audiobook fittingly called Crossing the Chasm. The book explores the challenges faced by nacent technologies on their way to mainstream adoption.

Crossing the chasm

In the years that followed, I couldn't help noticing the disconnect between Nix's devoted user base and its apparent lack of widespread adoption in the broader tech community.

Over 2021 and 2022, I focused my efforts and started nix.dev, a resource for practical and accessible Nix tutorials, thinking that good documentation was the thing holding Nix back. Eventually, I came to the realization that improving documentation alone will only get us so far.

We needed to fix the foundations.

We needed to remove things to reduce the cognitive load when using Nix.

The negative feedback loop

Over the years at Cachix we've talked to team after team abandoning Nix and have observed a surprisingly consistent pattern.

Nix is initially introduced by someone enthusiastic about the technology. Then, faced with a steep adoption curve, it is abandoned after backlash from the rest of the team.

Making it trivial for a project to adopt and maintain a development environment is crucial for other team members to see the benefits of Nix.

For example, Shopify was vocal about Nix way back in 2020, but eventually went quiet. Having companies like Shopify adopt Nix would be a major step forward for the whole ecosystem.

An interface as the heart of the Developer Experience

Since the 0.1 release two years ago, we've been rapidly iterating on a declarative interface for developer environments. We now have support for over 50 languages and 30 services:

devenv.nix
{ pkgs, config, ... }: {
  packages = [
    pkgs.cargo-watch
  ];

  languages.rust = {
    enable = true;
    channel = "nightly";
    rustflags = "-Z threads=8";
    targets = [ "wasm32-unknown-unknown" ];
  };

  processes = {
    backend.exec = "cargo watch -x run";
  };

  services = {
    postgresql.enable = true;
  };
}

With the introduction of tasks in the 1.2 release and Nix caching in 1.3, we're pretty happy with the devenv command-line interface and the extensible nature of the module system.

The modular architecture of the module system allows for seamless addition, modification, and removal of configuration options. This flexibility extends to defining your own options for the software you're writing.

Why do we need a Nix rewrite?

We've been using the Nix command-line interface under the hood as a low-level API to the evaluator and Nix store. We would've preferred to use something akin to an SDK instead, however the command-line interface was the most sensible interface two years ago out of the available options.

The new C FFI (Foreign Function Interface) could potentially grow into a viable solution, but it would necessitate substantial development effort and still leave us vulnerable to memory-safety issues. Moreover, the architecture of the Nix codebase is structured more as a monolithic framework rather than a modular library.

Ideally, if we're committing to fixing the developer experience over the next years, we'd want to have Nix implemented as a library in Rust.

Fortunately, such a project already exists and it's called Tvix. Started by flokli and tazjin in "Rewriting Nix", Tvix is a re-implementation of Nix in Rust, offering both memory-safety and a library-oriented architecture with independently usable components. Leveraging Rust's abstractions and ecosystem (e.g. tracing.rs), Tvix is positioned to significantly enhance the developer experience for devenv developers and users.

There are many architectural differences besides the obvious "Rewrite In Rust" cliche, so we'll talk about them as we start replacing our Nix command-line calls with Tvix libraries, starting with the evaluator.

Integrating the Tvix evaluator

The Nix evaluator directly traverses the abstract syntax tree (AST) during evaluation, while Tvix uses a bytecode virtual machine crafted according to the Crafting Interpreters book.

Tvix compiles Nix code into compact bytecode, then executes it in a virtual machine. This two-step approach offers potential performance benefits and optimization opportunities, like many other interpreted languages.

When you re-evaluate devenv.nix, you're most likely changing devenv.nix and not one of the few dozen Nix files that come from the devenv repository, or even the few thousand Nix files from the nixpkgs repository that could all be cached as bytecode.

In order to integrate the Tvix evaluator with devenv we'll need to:

  • Finish implementing builtins.fetchTree, where we have some ideas on how to simplify the caching layer and get rid of the annoying dependency on GitHub's rate-limited api.github.com endpoint.
  • Implement an evaluation debugger that will allow inspecting a program's state in case of errors.
  • Finish implementing tvix-eval-jobs that will be used for regression tests against nixpkgs to make sure that the evaluator behaves correctly.
  • Create debug tooling for when we discover regressions in the evaluator.
  • Integrate a nix-daemon layer to schedule builds.

We also recently streamed a Let's explore the Tvix evaluator video for those interested in digging into the code.

Using language-specific package managers as the build system

Once we've integrated the evaluator, we can finally generalize building languages using Nix reproducible builds by running the underlying build system to generate Nix expressions:

graph TD
A[devenv];
A -->|Rust| C[Cargo];
A -->|JavaScript| D[npm];
A -->|PHP| E[Composer];
C -->|Cargo.lock| F{Nix};
D -->|package.json| F{Nix};
E -->|composer.lock| F{Nix};

In Build Systems à la Carte, Nix is labelled as a suspending task scheduler.

In the general case, the dependency graph is computed statically, but a dependency can declare its dependencies dynamically as part of the build by returning more Nix code.

That's when evaluation and build phases start to mix, with evaluation depending on the result of a build, which is typically called import from derivation (as the naming comes from the implementation).

sequenceDiagram
    autonumber
    participant NixEvaluator as Nix evaluator
    participant NixStore as Nix store

    NixEvaluator->>NixEvaluator: evaluate
    NixEvaluator->>NixStore: write derivation
    NixStore->>NixStore: build
    NixStore->>NixEvaluator: read derivation output
    NixEvaluator->>NixEvaluator: evaluate

Since evaluation in Nix is single-threaded, the process described above gets blocked on each build requested during evaluation.

Implementing parallel evaluation in Tvix, after we figure out the architectural details of how it should work, will unlock the ability to support automatic conversion of language-specific build systems into Nix without sacrificing neither the developer experience, nor memory safety.

Final Words

As we embark on this new chapter with Tvix, I'm reminded of the journey that brought us here. It's been a decade since I wrote the we can do better blog post, highlighting the potential for improvement in configuration management and development environments, and I'm glad to see it all finally coming together.

Keep an eye out for updates and join the discussion:

Domen

devenv 1.3: Instant developer environments with Nix caching

Hot on the heels of the previous release of tasks, we're releasing devenv 1.3! 🎉

This release brings precise caching to Nix evaluation, significantly speeding up developer environments.

Once cached, the results of a Nix eval or build can be recalled in single-digit milliseconds.

If any of the automatically-detected inputs change, the cache is invalidated and the build is performed.

Caching comparison

Note

If you run into any issues, run devenv with --refresh-eval-cache and report an issue.

How does it work?

Behind the scenes, devenv now parses Nix's internal logs to determine which files and directories were accessed during evaluation.

This approach is very much inspired by lorri, but doesn't require a daemon running in the background.

The caching process works as follows:

  1. During Nix evaluation, devenv parses the Nix logs for any files and directories that are accessed.
  2. For each accessed path, we store:
  3. the full path
  4. a hash of the file contents
  5. the last modification timestamp

This metadata is then saved to a SQLite database for quick retrieval.

When you run a devenv command, we:

  1. Check the database for all previously accessed paths
  2. Compare the current file hashes and timestamps to the stored values
  3. If any differences are detected, we invalidate the cache and perform a full re-evaluation
  4. If no differences are found, we use the cached results, significantly speeding up the process

This approach allows us to efficiently detect changes in your project, including:

  • Direct modifications to Nix files
  • Changes to imported files or directories
  • Updates to files read using Nix built-ins, like readFile or readDir

Comparison with Nix's built-in flake evaluation cache

Nix's built-in flake evaluation caches outputs based on the lock of the inputs, ignoring changes to Nix evaluation that often happen during development workflow.

Comparison with existing tools

Let's take a closer look at how devenv's new caching system compares to other popular tools in the Nix ecosystem. Running our own cache gives us more control and visibility over the caching process, and allows us to improve our integration with other tools, like direnv.

lorri

While lorri pioneered the approach of parsing Nix's internal logs for caching, devenv builds on this concept, integrating caching as a built-in feature that works automatically without additional setup.

direnv and nix-direnv

These tools excel at caching evaluated Nix environments, but have limitations in change detection:

  • Manual file watching: Users often need to manually specify which files to watch for changes.
  • Limited scope: They typically can't detect changes in deeply nested imports or files read by Nix built-ins.

To leverage devenv's caching capabilities with direnv, we've updated the .envrc file to utilize devenv's new caching logic.

If you currently enjoy the convenience of our direnv integration to reload your development environment, make sure to update your .envrc to:

source_url "https://raw.githubusercontent.com/cachix/devenv/82c0147677e510b247d8b9165c54f73d32dfd899/direnvrc" "sha256-7u4iDd1nZpxL4tCzmPG0dQgC5V+/44Ba+tHkPob1v2k="

use devenv

to benefit from the new caching system.

What's next?

nix develop currently remains the last bit that's rather slow and uncacheable, particularly on macOS. We're working on bringing its functionality in-house to further bring down the overhead of launching a cached shell to under 100ms.

Join us on Discord if you have any questions,

Domen & Sander

devenv 1.2: Tasks for convergent configuration with Nix

For devenv, our mission is to make Nix the ultimate tool for managing developer environments. Nix excels at congruent configuration, where the system state is fully described by declarative code.

However, the real world often throws curveballs. Side-effects like database migrations, one-off tasks such as data imports, or external API calls don't always fit neatly into this paradigm. In these cases, we often resort to convergent configuration, where we define the desired end-state and let the system figure out how to get there.

To bridge this gap and make Nix more versatile, we're introducing tasks. These allow you to handle those pesky real-world scenarios while still leveraging Nix's powerful ecosystem.

Tasks interactive example

Usage

For example if you'd like to execute python code after virtualenv has been created:

devenv.nix
{ pkgs, lib, config, ... }: {
  languages.python.enable = true;
  languages.python.venv.enable = true;

  tasks = {
    "python:setup" = {
      exec = "python ${pkgs.writeText "setup.py" ''
          print("hello world")
      ''}";
      after = [ "devenv:python:virtualenv" ];
    };
    "devenv:enterShell".after = [ "python:setup" ];
  };
}

python:setup task executes before devenv:enterShell but after python:virtualenv task:

For all supported use cases see tasks documentation.

Task Server Protocol for SDKs

We've talked to many teams that dropped Nix after a while and they usually fit into two categories:

  • 1) Maintaining Nix was too complex and the team didn't fully onboard, creating friction inside the teams.
  • 2) Went all-in Nix and it took a big toll on the team productivity.

While devenv already addresses (1), bridging the gap between Nix provided developer environments and existing devops tooling written in your favorite language is still an unsolved problem until now.

We've designed Task Server Protocol so that you can write tasks using your existing automation by providing an executable that exposes the tasks to devenv:

devenv.nix
{ pkgs, ... }:
let
  myexecutable = pkgs.rustPlatform.buildRustPackage rec {
    pname = "foo-bar";
    version = "0.1";
    cargoLock.lockFile = ./myexecutable/Cargo.lock;
    src = pkgs.lib.cleanSource ./myexecutable;
  }
in {
  task.serverProtocol = [ "${myexecutable}/bin/myexecutable" ];
}

In a few weeks we're planning to provide Rust TSP SDK with a full test suite so you can implement your own abstraction in your language of choice.

You can now use your preferred language for automation, running tasks with a simple devenv tasks run <names> command. This flexibility allows for more intuitive and maintainable scripts, tailored to your team's familiarity.

For devenv itself, we'll slowly transition from bash to Rust for internal glue code, enhancing performance and reliability. This change will make devenv more robust and easier to extend, ultimately providing you with a smoother development experience.

Upgrading

If you run devenv update on your existing repository you should already be using tasks, without needing to upgrade to devenv 1.2.

Domen