Skip to content

Dev Shells

Note

Flake development shells are used to set up an shell environment into which one or more packages are "installed", making their binaries and libraries available.

Devshells may also define environment variables and shell hooks - bits of shell code which are automatically executed on entering the environment with nix develop.

Our first development shell

The following defines a small development shell which provides the hello package:

example dev shell
{
  description = "nix devshell example";

  inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixos-22.11"; };

  outputs = { self, nixpkgs }:
    let
      allSystems = [
        "x86_64-linux" # 64bit AMD/Intel x86
        "aarch64-darwin" # 64bit ARM macOS
      ];

      forAllSystems = fn:
        nixpkgs.lib.genAttrs allSystems
        (system: fn { pkgs = import nixpkgs { inherit system; }; });

    in {
      # nix develop <flake-ref>#<name>
      # -- 
      # $ nix develop <flake-ref>#first
      devShells = forAllSystems ({ pkgs }: {
        myshell = pkgs.mkShell {
          name = "nix";
          nativeBuildInputs = [ pkgs.hello ];
        };
      });
    };
}

Nix & systems

Note that we wrap the devShells attribute set in a call to a helper function, forAllSystems, which effectively calls the function fn once per entry in the systems list.

Note

The forAllSystems calls a function provided by nixpkgs, genAttrs. You can find documentation for this or other functions from nixpkgs and builtins by searching for the function in question on https://noogle.dev.

Note that the entry in noogle also provides a link to the source code. This is often the best way to understand what a function does.

This trick is necessary because outputs involving binaries in nix are tied to a system - in Nix, a system is a combination of hardware architecture and a operating system. E.g. aarch64-linux, x86_64-linux or similar.

We wrap system-dependent definitions, like devShells with this call to avoid having to repeat largely identical definitions for each system we want to support.

To see the difference, consider this example which defines the same flake by manually typing out each output entry:

devshell - manually define entries for each system
{
  description = "nix devshell example";

  inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixos-22.11"; };

  outputs = { self, nixpkgs }: {
    # nix develop <flake-ref>#<name>
    # -- 
    # $ nix develop <flake-ref>#first
    devShells.x86_64-linux = let pkgs = nixpkgs.legacyPackages.x86_64-linux;
    in {
      # define entries for x86 64bit AMD/Intel Linux Machines
      myshell = pkgs.mkShell {
        name = "nix";
        nativeBuildInputs = [ pkgs.hello ];
      };
    };

    devShells.aarch64-darwin = let pkgs = nixpkgs.legacyPackages.aarch64-darwin;
    in {
      # define entries for 64bit ARM macOS machines
      myshell = pkgs.mkShell {
        name = "nix";
        nativeBuildInputs = [ pkgs.hello ];
      };
    };
  };
}

Try it out

The flake shown above is hosted at https://github.com/nix4noobs/devshells-ex. Try it out by changing into a new directory and running the command: nix develop github:nix4noobs/devshells-ex#myshell

This specifically requests to run the myshell development shell (as given when using the nix develop command) in the nix4noobs/devshells-ex repo.

For more information on how flake reference URI's are expressed, see flake refs.

Second example - multiple dev shells

Our second example is hosted at https://github.com/nix4noobs/devshells-ex2.

The flake is defined like so:

flake with 2 devshells
{
  inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixos-22.11"; };

  outputs = { self, nixpkgs, flake-utils }:
    let
      allSystems =
        [ "x86_64-linux" "x86_64-darwin" "aarch64-linux" "aarch64-darwin" ];

      forAllSystems = fn:
        nixpkgs.lib.genAttrs allSystems
        (system: fn { pkgs = import nixpkgs { inherit system; }; });

    in {
      devShells = forAllSystems ({ pkgs }: {
        blue = pkgs.mkShell {
          name = "blue";
          nativeBuildInputs = [ pkgs.cowsay ];
          LANG = "C";
          AWESOME_TEAM = "blue";
          shellHook = ''
            echo "blue shell!"
            cowsay "team $AWESOME_TEAM rocks!"
          '';
        };
        red = pkgs.mkShell {
          name = "red";
          nativeBuildInputs = [ pkgs.fortune ];
          shellHook = ''
            echo "red shell!"
            fortune
          '';
        };
      });
    };
}

The flake defines two development shells, blue and red. Note how these shells set the shellHook to a multi-line string containing a short shell script to execute when logging into the shell. Note also that any upper-cased attributes given to the devshell are treated as environment variables to be defined in the shell.

Try launching the blue shell by running the following command: nix develop github:nix4noobs/devshells-ex2#blue

From inside the shell, try echo $AWESOME_TEAM to see the value of the environment variable defined in the flake passed on to the environment.

Secondly, try type cowsay - note how the cowsay binary is sourced from inside the nix store (/nix/store/<hash>/bin/cowsay). Similarly, note how type fortune will complain that fortune could not be found (unless you have installed it on your host OS).

Then exit the shell by typing exit.

Finally, try the red shell by running: nix develop github:nix4noobs/devshells-ex2#red. From there, note that type fortune shows that fortune is sourced from the nix store while type cowsay should complain that cowsay cannot be found (unless you have it installed on your host OS).

The point here is that development shells may provide different sets of packages, environment variables and shell scripts.

Warning

Development environments are impure in the sense that while binaries provided by nix take precedence, host-OS installed binaries can still be accessed.

This is really nice when you want to access your preferred editor (vim or emacs - no excuses!), but take care when building development environments. E.g. to create a development environment for C coding, be sure to install a compiler, if using pkg-tool to get linker and include flags, install a local copy to prevent querying your host-OS's pc files, and so on.