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:
{
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:
{
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:
{
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.