While packages are the most common type of item you'll find contained in flakes,
another type of item is a development shell. This is an environment that you can
enter with the nix develop
command. By default, each package will provide a development
shell which is the build environment used to build it. By running just nix develop
without specifying which shell is wanted, nix
will put you in a bash
shell with the
environment from the default package.
However, sometimes you may wish to set up additional tools in a development environment,
for example you may have scripts written in Python and want to have Python available as
for a developer to use, yet you do not want to include Python as a dependency in your built
package. Or you might want to set some environment variables so that logging is set to
debug mode in your development environment.
You can customise the nix develop
environment with the devShells
attribute set in a
flake. The helper function pkgs.mkShell
is useful here. This is an extended version of
the mkDerivation
function that has been used in previous examples, except this version
is specialised for shells. The following flake shows an example:
flake.nix
{
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
outputs = { self, nixpkgs }:
let
pkgs = import nixpkgs { system = "x86_64-linux"; };
in
{
devShells.x86_64-linux.default = pkgs.mkShell {
packages = [
pkgs.cargo
pkgs.rustc
pkgs.python3
];
RUST_LOG = 1;
};
};
}
Now if you run nix develop
in the same directory as this flake, you'll be left at a bash
shell with cargo, python3 and rustc available. If you run the command env | grep RUST
, you
can observe the following output:
$ env | grep RUST
RUST_LOG=1
Or which python3
:
/nix/store/xcaaly5shfy227ffs8nipxrd49b56iqq-python3-3.10.8/bin/python3
You could even define multiple shells. For example, lets say you wanted to support Python 3.8
through 3.10 in a single project. nixpkgs
provides python artifacts in specific names like
python38
if you want specific versions. So you could build a flake which defines your python
versions for each one, like so:
{
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
outputs = { self, nixpkgs }:
let
pkgs = import nixpkgs { system = "x86_64-linux"; };
pythonVersions = {
python38 = pkgs.python38;
python39 = pkgs.python39;
python310 = pkgs.python310;
default = pkgs.python310;
};
makePythonShell = shellName: pythonPackage: pkgs.mkShell {
packages = [ pythonPackage ];
shellHook = ''
echo "Now entering ${shellName} environment."
'';
};
in
{
devShells.x86_64-linux = builtins.mapAttrs makePythonShell pythonVersions;
};
}
Now if you run the command nix develop .#python38
, you will end up in a shell that
contains Python 3.8, something you can confirm with python --version
.
$ nix develop .#python38
Now entering python38 environment.
$ python --version
Python 3.8.15
You can also inspect all items that are available in the flake with the command
nix flake show
:
$ nix flake show
git+file:///home/tony/code/nix-guide?dir=8-dev-envs%2fmanypython
└───devShells
└───x86_64-linux
├───default: development environment 'nix-shell'
├───python310: development environment 'nix-shell'
├───python38: development environment 'nix-shell'
└───python39: development environment 'nix-shell'
But what if I don't want bash?
Of course, you might prefer other shells than bash
- however, Nix seems hardcoded
to use bash
. So you might wonder, how can you override the shell? For example, you
might prefer the zsh
or fish
shells.
The first method of doing this requires no external tools, just Nix
itself. You
install your desired shell in the your devShell
, and execute it in the shellHook
.
Here's an example that starts the fish shell.
{
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
outputs = { self, nixpkgs }:
let
pkgs = import nixpkgs { system = "x86_64-linux"; };
in
{
devShells.x86_64-linux.default = pkgs.mkShell {
packages = [
pkgs.fish
];
shellHook = ''
fish && exit
'';
};
};
}
While the big advantage here is that you don't need any tools apart from nix
,
one big disadvantage is that your shell of choice needs to be included in each
development environment, and other users of your flakes are forced to use your
preferred shell also.
Another option is to run nix develop -c fish
. This overrides the command on
shell launch to launch your preferred shell. (Thanks to u/Mysteriox7
on reddit for bringing this to my attention). This is an improvement over forcing
all users of your flake to use your preferred shell, but does need to be typed each time.
With both approaches however, one thing that can be a pain is they require typing that
command every time you enter a project which you want to use nix develop
for.
Luckily, there are third party tools to the rescue.
nix-direnv
nix-direnv
is a integration between nix
and the direnv
tool. direnv
is a
command to run commands whenever you enter a directory with a .envrc
file.
nix-direnv
builds upon this to load a cached nix
environment into your currently
open shell. Effectively, the first time you open a project with nix-direnv
, it will
build your development environment, record the environment changes this process makes
then apply those to your current shell. On opening the project again in the future,
it will load the cached environment, so it doesn't need to execute your nix scripts
again if they are unchanged,
To use this, you'll need to install direnv and nix-direnv. Follow their
installation instructions at the links provided. Then to set up your flake project
with direnv, you just need to create a .envrc
file with one command, use flake
.envrc
use flake
Alternatively, you can pass it a flake reference if you want to use a different
devShell. For example, my hosted version of the Python 3.8 development shell can
be used with
use flake "gitlab:tonyfinn/nix-guide?dir=8-dev-envs/manypython#python38"
That's it for this part of the Nix guide. Next time
I'll cover another type of flake output - runnable commands.