devenv

repository·main·Indexed 27 days ago

https://github.com/cachix/devenv

A tool for creating fast, declarative, and reproducible developer environments using Nix. It allows developers to define languages, packages, services, and processes in a single configuration file. The project includes a Nix backend (devenv-nix-backend), a shell reload manager (devenv-reload), an integration testing framework (devenv-run-tests), and a terminal user interface (devenv-tui) for monitoring build and evaluation activity.

Tokens
155.3K
Snippets
414
Records
1.3K
Agent score
92%

What's inside devenv

  1. Overview of devenv-nix-backend

    main
    The devenv-nix-backend is the default Nix backend for devenv. It facilitates communication with Nix using C++ bindings (via nix-bindings-* crates). It manages a long-lived EvalState to maintain an in-process evaluation cache across multiple calls within a single devenv execution, improving performance.
  2. Understand Pinning and the Lockfile

    main

    To ensure developer environments are reproducible, devenv uses a lockfile called devenv.lock. Each input declared in devenv.yaml is resolved to an exact revision and stored in this lockfile. This prevents different machines from resolving floating branches or tags to different commits.

    There is no separate "create lock" step; devenv.lock is automatically created or updated whenever you run devenv commands on the project.

  3. Understand devenv auto-activation behavior

    main

    How it works

    The hook monitors directory changes and performs the following:

    1. Searches upwards from the current directory for a devenv.yaml or devenv.nix file.
    2. Verifies the project against the trust database.
    3. If trusted, it executes devenv shell in a subshell.

    Automatic deactivation

    When you cd out of the project directory or any of its subdirectories, the devenv subshell exits automatically, returning you to your normal shell.

    Re-entry protection

    To prevent nesting, the hook will not spawn a new environment if you are already inside a devenv shell for that same project. Navigating into subdirectories of the current project keeps the current shell active; only navigating outside the project triggers deactivation.

  4. Understand the Nix 'Stat Storm' and performance overhead

    main

    In Nix-based environments, starting programs can be slow due to a 'stat storm'. Because Nix stores packages in unique /nix/store/<hash>-<name>/lib directories rather than global paths like /usr/lib, the dynamic loader must search through many DT_RUNPATH entries to find shared libraries.

    For every directory in the DT_RUNPATH, the loader also probes glibc-hwcaps subdirectories (e.g., x86-64-v3), multiplying the number of failing openat() calls. This overhead is particularly noticeable on slow disks, network filesystems (NFS), or cold caches.

    Example of overhead:

    • devenv version (83 libraries, 12 DT_RUNPATH dirs) results in ~486 failing opens.
    • imagemagick (91 libraries, 35 DT_RUNPATH dirs) results in ~1225 failing opens.
  5. Understand devenv garbage collection roots

    main

    To prevent Nix from deleting active developer environments from the store, devenv maintains garbage collection (GC) roots.

    Each time you activate a shell or run a devenv command that evaluates your environment, a timestamped symlink is created inside $DEVENV_HOME/gc/ (which defaults to ~/.local/share/devenv/gc/). This symlink points to the Nix store path backing your environment. Only the latest successful invocation per project folder is kept; older generations are cleaned up automatically.

  6. Orchestrate process startup and shutdown using tasks

    main

    In devenv, all processes are exposed as tasks with the naming convention devenv:processes:<name>. You can use this to orchestrate startup and shutdown sequences by defining tasks that run before or after a specific process.

    Execute setup tasks before a process

    Use the before attribute in a task to ensure a command (like database migrations) runs before the target process starts.

    Run cleanup tasks after a process

    Use the after attribute in a task to ensure cleanup commands (like removing PID files or temporary directories) run after the target process stops.

    # Example: Running migrations before a backend process
    {
      processes.backend = {
        exec = "cargo run --release";
      };
    
      tasks."db:migrate" = {
        exec = "diesel migration run";
        before = [ "devenv:processes:backend" ];
      };
    }
  7. Define and activate profiles in devenv

    main

    Profiles allow you to organize different variations of your development environment. You can define them in your devenv.nix file using the profiles option. Profiles can be activated manually via the CLI or automatically based on system environment (hostname or username).

    To activate a single profile:

    devenv --profile <profile_name> shell

    To activate multiple profiles:

    devenv --profile <profile_1> --profile <profile_2> shell
    { pkgs, config, ... }: {
      profiles = {
        backend.module = {
          services.postgres.enable = true;
          services.redis.enable = true;
          env.ENVIRONMENT = "backend";
        };
    
        frontend.module = {
          languages.javascript.enable = true;
          processes.dev-server.exec = "npm run dev";
          env.ENVIRONMENT = "frontend";
        };
    
        testing.module = { pkgs, ... }: {
          packages = [ pkgs.playwright pkgs.cypress ];
          env.NODE_ENV = "test";
        };
      };
    }
  8. Handle native C/C++ libraries for Python packages

    main

    If Python packages (like Pillow or grpcio) fail to find native libraries, add those libraries to the global packages list. devenv will automatically add them to LD_LIBRARY_PATH (Linux) or DYLD_LIBRARY_PATH (macOS).

    For more granular control, use the libraries option.

    { packages = [ pkgs.cairo pkgs.zlib ];
    
      languages.python = {
        enable = true;
        venv.enable = true;
        venv.requirements = ''
          pillow
          grpcio-tools
        '';
      };
    }
  9. Manage secrets with SecretSpec

    main

    devenv 2.0 uses SecretSpec for declarative, provider-agnostic secrets management. You declare required and optional secrets in a secretspec.toml file. Developers can then provide these secrets via backends like keyring, dotenv, 1Password, or environment variables. This prevents secrets from being silently leaked to background processes or coding agents.

    # secretspec.toml
    [project]
    name = "myapp"
    revision = "1.0"
    
    [profiles.default]
    DATABASE_URL = { description = "PostgreSQL connection string", required = true }
    STRIPE_KEY = { description = "Stripe API secret key", required = true }
    SENTRY_DSN = { description = "Sentry error tracking DSN", required = false }
  10. Initialize and migrate to SecretSpec

    main

    You can set up SecretSpec by initializing a configuration or migrating an existing .env file.

    Initialize configuration

    Run the following command to select your preferred provider backend (e.g., keyring, onepassword, dotenv, env, lastpass) and your default profile:

    $ secretspec config init

    Migrate from .env

    To create a secretspec.toml file based on your existing .env file, use:

    $ secretspec init --from dotenv
    $ secretspec config init
    $ secretspec init --from dotenv
  11. Define outputs as custom module options

    main

    For greater flexibility, you can define outputs using the module system's options block. This allows you to integrate outputs into your configuration as typed options.

    When defining an option that represents an output, use config.lib.types.outputOf lib.types.package (or config.lib.types.output if you do not want to specify the output option type). devenv will automatically include these in the build process.

    { pkgs, lib, config, ... }: {
      options = {
        myapp.package = pkgs.lib.mkOption {
          type = config.lib.types.outputOf lib.types.package;
          description = "The package for myapp";
          default = import ./myapp { inherit pkgs; };
          defaultText = "myapp";
        };
      };
    
      config = {
        outputs.git = pkgs.git;
      };
    }