Declarative shell environments with shell.nix#

Overview#

Declarative shell environments allow you to:

  • Automatically run bash commands during environment activation

  • Automatically set environment variables

  • Put the environment definition under version control and reproduce it on other machines

What will you learn?#

In the Ad hoc shell environments tutorial, you learned how to imperatively create shell environments using nix-shell -p. This is great when you want to quickly access tools without installing them permanently. You also learned how to execute that command with a specific Nixpkgs revision using a Git commit as an argument, to recreate the same environment used previously.

In this tutorial we’ll take a look at how to create reproducible shell environments with a declarative configuration in a Nix file. This file can be shared with anyone to recreate the same environment on a different machine.

How long will it take?#

30 minutes

What do you need?#

  • Familiarity with the Unix shell

  • A rudimentary understanding of the Nix language

Entering a temporary shell#

Suppose we want an environment where cowsay and lolcat are available. The simplest possible way to accomplish this is via the nix-shell -p command:

$ nix-shell -p cowsay lolcat

This command works, but there’s a number of drawbacks:

  • You have to type out -p cowsay lolcat every time you enter the shell.

  • It doesn’t (ergonomically) allow you any further customization of your shell environment.

A better solution is to create our shell environment from a shell.nix file.

A basic shell.nix file#

Create a file called shell.nix with these contents:

 1let
 2  nixpkgs = fetchTarball "https://github.com/NixOS/nixpkgs/tarball/nixos-24.05";
 3  pkgs = import nixpkgs { config = {}; overlays = []; };
 4in
 5
 6pkgs.mkShellNoCC {
 7  packages = with pkgs; [
 8    cowsay
 9    lolcat
10  ];
11}
Detailed explanation

We use a version of Nixpkgs pinned to a release branch. If you followed the Ad hoc shell environments tutorial and don’t want to to download all dependencies again, specify the exact same revision as in the section Towards reproducibility:

1let
2  nixpkgs = fetchTarball "https://github.com/NixOS/nixpkgs/tarball/2a601aafdc5605a5133a2ca506a34a3a73377247";
3  pkgs = import nixpkgs { config = {}; overlays = []; };
4in

We explicitly set config and overlays to avoid them being inadvertently overridden by global configuration.

mkShellNoCC is a function that takes as argument an attribute set. Here we give it an attribute packages with a list containing two items from the pkgs attribute set.

Side note on mkShell

nix-shell and mkShell were originally conceived as a way to construct a shell environment containing the tools needed to debug package builds, such as Make or GCC. Only later it became widely used as a general way to make temporary environments for other purposes. mkShellNoCC is a function that produces such an environment, but without a compiler toolchain.

You may encounter examples of mkShell or mkShellNoCC that add packages to the buildInputs or nativeBuildInputs attributes instead. mkShellNoCC is a wrapper around mkDerivation, so it takes the same arguments as mkDerivation, such as buildInputs or nativeBuildInputs. The packages attribute argument to mkShellNoCC is simply an alias for nativeBuildInputs.

Enter the environment by running nix-shell in the same directory as shell.nix:

Note

The first invocation of nix-shell on this file may take a while to download all dependencies.

$ nix-shell
[nix-shell]$ cowsay hello | lolcat

nix-shell by default looks for a file called shell.nix in the current directory and builds a shell environment from the Nix expression in this file. Packages defined in the packages attribute will be available in $PATH.

Environment variables#

You may want to automatically export certain environment variables when you enter a shell environment.

Set GREETING so it can be used in the shell environment:

 let
   nixpkgs = fetchTarball "https://github.com/NixOS/nixpkgs/tarball/nixos-24.05";
   pkgs = import nixpkgs { config = {}; overlays = []; };
 in

 pkgs.mkShellNoCC {
   packages = with pkgs; [
     cowsay
     lolcat
   ];

+  GREETING = "Hello, Nix!";
 }

Any attribute name passed to mkShellNoCC that is not reserved otherwise and has a value which can be coerced to a string will end up as an environment variable.

Try it out! Exit the shell by typing exit or pressing Ctrl+D, then start it again with nix-shell.

[nix-shell]$ echo $GREETING

Warning

Some variables are protected from being set as described above.

For example, the shell prompt format for most shells is set by the PS1 environment variable, but nix-shell already sets this by default, and will ignore a PS1 attribute set in the argument.

If you need to override these protected environment variables, use the shellHook attribute as described in the next section.

Startup commands#

You may want to run some commands before entering the shell environment. These commands can be placed in the shellHook attribute provided to mkShellNoCC.

Set shellHook to output a colorful greeting:

 let
   nixpkgs = fetchTarball "https://github.com/NixOS/nixpkgs/tarball/nixos-24.05";
   pkgs = import nixpkgs { config = {}; overlays = []; };
 in

 pkgs.mkShellNoCC {
   packages = with pkgs; [
     cowsay
     lolcat
   ];

   GREETING = "Hello, Nix!";
+
+  shellHook = ''
+    echo $GREETING | cowsay | lolcat
+  '';
 }

Try it again! Exit the shell by typing exit or pressing Ctrl+D, then start it again with nix-shell to observe the effect.

References#

Next steps#