Nix 2.4+ introduces replacements for commonly known nix commands. The `nix-shell` replacements cause some confusion which this post aims to solve.
A new Nix command
This post follows the release of Nix 2.4, which among many other things introduced an all new (experimental) nix
command along with the previously discussed flake feature.
The nix
command aims to collect most common commands such as nix-build
, nix-copy-closure
, nix-env
, … as subcommands of one common program. Unsurprisingly, this does not spare nix-shell
.
Yet, unlike some of the other commands which received a more or less one-to-one replacement it is not so easy with nix-shell
. This command was actually broken up into multiple commands with different semantics: nix shell
, nix develop
and nix run
. Yet, depending on what you used nix-shell
for in the past the new commands may not exactly do what you would expect. Indeed, they happen to cause quite some confusion already. Most notably, while nix-shell
invoked without any other flags previously set up the build environment of a derivation (which could be somewhat abused to define general development environments) that is not what nix shell
will do…
What the shell…1
Yes, but from the start…
If you are here just for the commands you can read the tl;dr for nix develop
, nix shell
, or nix run
directly or jump to the notes section.
Development shells (nix-shell [DERIVATION]
)
Development shells using the new nix
command are now created with nix develop
.
That command is a more focused version with additional tools for development. It is meant to be used with flakes but supports standalone derivations using the -f
flag. How to use it then and what will it do?
A development aid
The intended use of nix develop
is to recreate build environments for single packages. That is, you call nix develop nixpkgs#hello
and are dropped into a shell that is as close as possible to the nix builder environment.
“yes,.. and?”
This allows you to build a package step by step, or better phase by phase, meaning that in particular the build instructions used by nix can be reused for development purposes. Thus, we can use nix without needing to repeatedly go through full build processes, including wasteful copying, configuring, etc.
To make this process even easier, nix develop
now comes with special arguments to run those phases directly.
NOTE
tl;dr
nix develop
creates a shell with all buildInputs
and environment variables of a derivation loaded and shellHook
s executed.
This allows to run phases individually in the shell using $ unpackPhase
, $ configurePhase
$ buildPhase
, etc.
…or directly using nix develop --<PHASE>
or nix develop --phase PHASE
(for non-standard phases).
…or run an arbitrary command in the shell using nix develop --command COMMAND [ARGS…]
Why should you use this?
It is most useful for locally developed packages.
Use it to set up the environment using e.g. the configurePhase
and perform the subsequent development using build
and check
phases. If your workflow includes things that are not part of a phase use nix develop --command
In essence, this is exactly what nix-shell
was intended for!
WARN
A slight annoyance with phases arises when the targeted derivation overrides standard phases, i.e. {unpack,configure,build,install}Phase
s. As the default implementation in nix’s stdenv
is done as functions, an internal use of runHook
will give precedence to those functions over the overridden phases stored as environment variables.
Solution
Enter a shell using nix develop
and run the overridden phases using eval $buildPhase
or --command eval ‘$buildPhase’
.
Practically, nix-shell
was also used for another purpose; reproducible development environments.
Development environments
Particularly useful combined with tools like direnv
, one can leverage the fact that the resulting shell of nix-shell
nix develop
includes all declared buildInputs
and environment variables to put together an environment with all sorts of dependencies and development tools available. Importantly, nix will also ensure all setupHook
s are run when the shell is opened allowing for some impure setup to happen.
A helpful tool to achieve this is mkShell
. This function provides an easy interface to collect packages for an environment.
pkgs.mkShell = {
# a list of packages to add to the shell environment
packages ? [ ]
, # propagate all the inputs from the given derivations
inputsFrom ? [ ]
, buildInputs ? [ ]
, nativeBuildInputs ? [ ]
, propagatedBuildInputs ? [ ]
, propagatedNativeBuildInputs ? [ ]
, ...
}: ...
All extra attributes unknown to mkDerivation
are applied as env variables.
You can provide a shellHook
to run commands whenever you enter the shell
Being closely connected to flakes, nix develop
supports loading a flake’s development shell directly if a devShell
output is defined.
HELP
Example
{
description = "Flake utils demo";
inputs.nixpkgs.url = "github:nixos/nixpkgs";
inputs.flake-utils.url = "github:numtide/flake-utils";
outputs = { self, nixpkgs, flake-utils }:
let
in
flake-utils.lib.eachDefaultSystem (
system:
let
pkgs' = import nixpkgs { inherit system; };
in
rec {
packages = { myPackage = ... }; # packages defined here
devShell = pkgs'.mkShell = {
# a list of packages to add to the shell environment
packages ? [ jq ]
, # propagate all the inputs from the given derivations
# this adds all the tools that can build myPackage to the environment
inputsFrom ? [ pacakges.myPackage ]
};
}
);
}
Temporary Programs
The fact that nix is built on the idea of the nix store from which user environments are created by cherry-picking the desired packages may raise the question, whether we may be able to amend our current environment imperatively2. And in fact, we can. It is possible to add software to a user’s profile by imperatively by the means of or nowadays nix-env -iA
nix profile install
.
WARN
Beware that nix profile
is incompatible with nix-env
and therefore (today) also with home-manager
.
Yet, we also know that installing software this way is not the Nix way of doing things.
For the times when we do want to have some program at our disposal, either to try it out, use a different version or just needing it only temporarily, there should be a way to get this without going through the effort of adding it to your configuration.nix
, home.nix
, project default.nix
, etc. and rebuilding your environment. In these cases, traditional distributions reach back to installing software or relying on containerization (i.e. docker). In Nix, while you could install the piece of software, the aforementioned usage of the nix store allows making software available temporarily without installing it.
This is what nix-shell -p <package+>
is used for. nix-shell
will retrieve the desired packages and open a shell with these packages mixed in.
HELP
Example
Given you need to quickly convert some asciidoc
to HTML usually the response of your terminal will be
$ asciidoc -b html5 manual.adoc
zsh: command not found: asciidoc
While you could go and add asciidoc to your configuration you might need it just once. In that case we can make use of nix-shell
:
$ nix-shell -p asciidoc
[nix-shell] $ asciidoc -b html5 manual.adoc
This should now just work. Likewise, this works with almost anything available to install through nixpkgs
.
INFO
Under the hood
Internally, what happens when nix-shell -p asciidoc
is called is that nix constructs a derivation with the programs as buildInputs
and popularizes them through the same mechanism described above.
In this case the derivation shell’ed into is:
with import <nixpkgs> { }; (pkgs.runCommandCC or pkgs.runCommand) "shell" { buildInputs = [ (asciidoc) ]; } ""
Note that instead of derivations you can also add expressions as the -p
argument as these are just plugged in, i.e.:
$ nix-shell -p "import ./some.nix {}"
But this is not about nix-shell
…
Nix 2.4 allows the same thing using nix shell
now focussing on flakes.
With nix shell
any output of a flake can be added to the environment by running
$ nix shell nixpkgs#asciidoc
Here, nix strictly expects a flake URL.
Like nix-shell
this command supports multiple arguments.
NOTE
tl;dr
nix shell
creates a shell from the specified inputs.
This is useful to install temporary software
…from a flake specifier.
…from a *.nix
file/arbitrary expression using --impure --expr EXPR
flags
Why should you use this?
The strong point about Nix is its declarative way to manage installations. Software that is used constantly can and should be packaged by the respective tool, be it a system configuration, home configuration.
For project development tools one can use development shells as discussed above.
Yet, sometimes a program or library is needed temporarily only, or once in a different version etc. In these cases programs can be loaded into the shell using nix shell
. Derivations from this kind command are eventually garbage collected and removed from the nix store, so they do not use up dist space unnecessarily.
WARN
Notably, not mentioned here is the use of nix shell
to load FANCYLANGUAGE
with FANCYLANGUAGEPACKAGES
. Sadly, this hits the limits of the new command. See the section about shellHook
s below.
Run scripts
Apart from dropping into development shells, nix-shell
can also be used to run commands and programs from derivation not currently installed to the user’s profile. This is it can build a shell as before and run a command inside transparently.
We discussed the use of nix-shell --command COMMAND ARGS
above, where we would run a command from within the build environment of a derivation. Similarly, we may want to just run a program provided by a derivation. For this nix-shell
provided the --run
argument
INFO
“–command
” vs “run
”
As a development aid, --command
is interactive, meaning among other things, that if a command fails or is interrupted by the user, the user is dropped into the shell with the build environment loaded.
This behavior translates into an invocation using the -p PROGRAM
argument as well as seen in the following box.
$ asciidoc
zsh: command not found: asciidoc
$ nix-shell -p asciidoc --command asciidoc
Man page: asciidoc --help manpage
Syntax: asciidoc --help syntax
$ asciidoc # still available as were in the build shell with asciidoc present
Man page: asciidoc --help manpage
Syntax: asciidoc --help syntax
--run
runs non-interactive and closes the shell after the command returns
$ asciidoc
zsh: command not found: asciidoc
$ nix-shell -p asciidoc --run asciidoc
Man page: asciidoc --help manpage
Syntax: asciidoc --help syntax
$ asciidoc # not available anymore
zsh: command not found: asciidoc
nix shell -c
As the functions of nix-shell DERIVATION
and nix-shell -p DERIVATION
were separated, the new tools come with new clearer semantics.
The generic nix-shell --run
function is now nix shell -c
. Given an installable, nix allows to run any command in an environment where the installable is present. Note that this command is run in a non-interactive shell. The shell is dropped as the command ends.
The above example using the new command would look like this:
$ nix shell nixpkgs#asciidoc -c asciidoc
nix run
Yet, nix shell -c
will still require to type the name of the executed program. As for most programs this command is the same as the derivation name e.g. nix shell nixpkgs#asciidoc -c asciidoc
another command was introduced named nix run
. With nix run
the previous command can be run as nix run nixpkgs#asciidoc
.
Naturally, the functionality of nix run
goes further and as is the case for many other new commands mainly concerns flakes. Next to packages
, nixosModules
and others, flakes can now also define apps
. Written as records with two fields -type
(currently, necessarily app
) and program
(an executable path) - these apps can be run directly using nix run
.
This app definition
...
outputs = {self}: {
...
apps.x86_64-linux.watch = {
type = "app";
program = "${generator-watch}";
};
}
… could be used to watch and build this blogs source directly using nix run .#watch
, given generator-watch
is a script in the nix store. Note that program
only accepts paths in the store and no default arguments.
If an attribute to nix run
is not found as an app, nix will look up a program
using this key instead and execute programs.${<program>}/bin/<program>
instead.
NOTE
tl;dr
The new nix
command comes with a new way to run programs not installed in your system for an even greater “run and forget” experience.
With nix shell DERIVATION+ -c COMMAND
… run any command in an environment with all specified DERIVATION
s present (again consider the section about shellHook
s)
With nix run INSTALLABLE
you can
… run scripts defined in a flake under the apps
output
… run the executables of any derivation as long as it is located in bin/INSTALLABLE
of the derivation with the attribute name INSTALLABLE
Shell interpreter #!/usr/bin/env nix-shell
Lastly, a useful feature of nix-shell
is its usage as a shell interpreter. What that means is that nix-shell
can be used to dynamically fetch dependencies for a script file and execute the file in that context. Shell interpreters are defined using a special syntax at the start of script files.
INFO
Shebang Interpreter Line
Bash, Zsh and other shells interpret a first line starting with #!
as an interpreter instruction. This way, instead of running a file using e.g. bash or python explicitly, we can write #!/usr/bin/env python
to instruct the shell to use python to execute the file.
#!/usr/bin/env python
# file.py
print("hello")
Here $ python file.py
would be equivalent to ./file.py
, given the user has permission to execute the file.
The nix-shell
command can be used to use nix to provide the actual interpreter using
#! /usr/bin/env nix-shell
#! nix-shell -i real-interpreter -p packages
Therefore, the above example would therefore continue to work even without python installed, if defined as
#! /usr/bin/env nix-shell
#! nix-shell -i python -p python3
print("hello")
As it stands this function does not yet have a replacement.
Notes and Resources
To flake or not to flake
Most of the new nix commands are designed in a flake first way. Most notably nix {shell,develop,run}
expect flake URLs as argument. Traditional *.nix
files can be used with the --expr
argument all commands support. As flake mode imposes greater purity strictness, imports have to happen with the --impure
flag given:
$ nix shell --impure --expr "import my.nix {}"
A word about shellHook
s
While shell hooks were previously introduced as a means to run certain commands to set up a development shell with nix develop
and nix-shell
, they find use in other places. Concretely, neither nix run
nor nix shell
are running these hooks. This is even though their nix-shell
equivalent does so.
What that means is that nix shell
cannot be used to load e.g. a Python environment with packages directly as python modules use the shellHook
to set up the PYTHONPATH
. The same goes for Perl and other ecosystems with similar principles. This means
nix shell nixpkgs#{python,python3Packages.numpy}
and
nix-shell -p python python3Packages.numpy
Do not evaluate to the same thing.
Instead, one needs to work around with custom expressions or mkShell
s passed to nix develop.
Workarounds
For Python in particular we can use python3.withPackages
to build an ad-hoc derivation with the paths set up correctly:
nix shell --impure --expr "with import <nixpkgs> {}; python.withPackages (pkgs: with pkgs; [ prettytable ])"
This might work as well with other language ecosystems
Alternatively, we can (ab)use nix develop
by passing it a devShell
:
nix develop --impure --expr "with import <nixpkgs> {}; pkgs.mkShell { packages = [python3 python3Packages.numpy ];}"
Both approaches work to some degree but are clunky (i.e. not improving UX as promised) and rely on the supposed-to-be-superseded channels.
The second-hardest problem
Picking up the confusion mentioned in the beginning, there is another problem with nix shell
… naming. Being so closely named to its semantically different predecessor, it is impossible to query google for meaningful, targeted results. This is a pain for newcomers and more experienced nix’ers alike. And indeed, there is a heated discussion on renaming the shell command. Yet, until that is resolved, I hope this guide helps to understand the differences a bit better.
newnixshellguide
Appendix
Summary table
/ |
nix-shell
|
nix develop
|
nix shell
|
nix run
|
---|---|---|---|---|
runs shellHook s
|
yes | yes | no | no |
use as interpreter | yes | no | no | no |
supports flakes | no | yes | yes | only |
evaluate nix file | yes |
with --impure , -f or --expr
|
with --impure , -f or --expr
|
with --impure , -f or --expr
|
modifies environment |
PATH , attributes mkmkDerivation and changes by shellHooks
|
PATH , attributes mkmkDerivation and changes by shellHooks
|
PATH
|
nothing |