Development environment with nix-shell
Nix can be used to provide some kind of virtual environment through the nix-shell
command.
If you already have a nix package definition of your project it's easy: Just use nix-shell
instead of nix-build
and you will end up in a bash shell that reproduce the build-environment of your package. You can also override[1] your package in a shell.nix
file to add test and coverage dependencies, that are not necessary for the actual build of the package, but that you want for your development environment.
But, if you don't (or you don't want to) have a package definition you can still use a nix-shell to provide a reproducible development environment. To do so, you have to create a shell.nix
file at the root of your repository. For example, if you want to have Ruby 3.2 and not one provided by your system you can write:
{ pkgs ? import <nixpkgs> {} }:
pkgs.mkShell {
# nativeBuildInputs is usually what you want -- tools you need to run
nativeBuildInputs = with pkgs.buildPackages; [ ruby_3_2 ];
}
Then just run:
$ nix-shell
Or, to be more explicit:
$ nix-shell shell.nix
Now you have ruby 3.2 available in your shell:
$ ruby --version
ruby 3.2.2 (2023-03-30 revision e51014f9c0) [x86_64-linux]
To be sure that the tools installed on your system will not interfere with the dependencies that you've defined in the shell you can use the --pure
option.
If you'd like to load a local nix expression into a shell you can do it by modifying the earlier example a little bit:
{ pkgs ? import <nixpkgs> {} }:
pkgs.mkShell {
# buildInputs is for dependencies you'd need "at run time",
# were you to to use nix-build not nix-shell and build whatever you were working on
buildInputs = [
(import ./my-expression.nix { inherit pkgs; })
];
}
If you want to see how to manually run the various phases of a given derivation from a nix-shell (useful to debug), see Nixpkgs/Create_and_debug_packages#Using_nix-shell_for_package_development.
nix develop
For Flakes-based projects (flake.nix
file in project root),
we replace nix-shell
with nix develop
Example: Building Nix in a development shell, to get Incremental builds = faster recompiles
git clone https://github.com/NixOS/nix --depth 1 cd nix nix develop
Now what? Let's read the manual:
less README.md less doc/manual/src/contributing/hacking.md
The contributing guide for Nix says:
To build all dependencies and start a shell in which all environment variables are set up so that those dependencies can be found: ```console $ nix-shell ``` To build Nix itself in this shell: ```console [nix-shell]$ ./bootstrap.sh [nix-shell]$ ./configure $configureFlags --prefix=$(pwd)/outputs/out [nix-shell]$ make -j $NIX_BUILD_CORES ```
So, in our nix develop
shell, we run
./bootstrap.sh ./configure $configureFlags --prefix=$(pwd)/outputs/out make -j $NIX_BUILD_CORES
This will compile Nix to ./outputs/out/bin/nix
Let's make some changes to the source code, and run make
again.
Now the compilation should be much faster (see Incremental builds)
stdenv.mkDerivation
Let's assume you have a default.nix
file
{ stdenv, python }:
stdenv.mkDerivation {
buildInputs = [ python ];
name = "some-package";
version = "0.0.1";
src = /home/yourname/path/to/project; # can be a local path, or fetchFromGitHub, fetchgit, ...
}
Then you can start a development shell with
nix-shell -E 'with import <nixpkgs> { }; callPackage ./default.nix { }'
In this shell, you can run the phases of stdenv.mkDerivation:
# clean build: copy sources from /nix/store
echo "src = $src" && cd $(mktemp -d) && unpackPhase && cd *
# dirty build: keep cache files from last buildPhase, to compile faster
# this is useful to make many small changes to a large project
# after each change, just run `buildPhase`
#cd $HOME/path/to/project
configurePhase
buildPhase # most time is spent here
checkPhase && installPhase && fixupPhase
cross env
The comments in the code snippets on nativeBuildInputs
and buildInputs
above might seem pedantic --- who cares about build-time vs run-time when we're just making a dev environment, not a real package! However, the distinction becomes of practical importance if one wants a cross compilation development environment. In that case one would begin file with something like:
{ pkgs ? import <nixpkgs> { crossSystem.config = "exotic_arch-unknown-exotic_os"; } }:
and nativeBuildInputs
would be for the native platform, while buildInputs
would be for the foreign platform. That's a much more practical distinction: any tool that's miscategorized one won't be able to run, and any library that's miscategorized one won't be able to link!
direnv
One of the limitations of nix-shell is that you can't use a shell other than bash. Thankfully, there is Direnv [[2]] with the support of Nix[[3]] to overcome this limitation. Also, Direnv provides some nice features like loading the environment automatically when you enter your project directory and show the loaded variables to you (explicit is always better;-)).
First, install Direnv:
nix-env -i direnv
Now, you need to add one more file in the root of your repository named .envrc
that contains only this:
use_nix
Then depending on the shell you are using, you need to add a line in your configuration file. See the Setup section of the doc[4]. For example, for Zsh put in your ~/.zshrc.local
:
eval "$(direnv hook zsh)"
Then, still at the root of your repository, run:
$ direnv allow .
direnv: loading .envrc
direnv: using nix
[...]
+SIZE +SOURCE_DATE_EPOCH +STRINGS +STRIP +TEMP +TEMPDIR +TMP +TMPDIR +_PATH +buildInputs +builder +checkPhase +cmakeFlags +configureFlags +doCheck +enableParallelBuilding +name +nativeBuildInputs +out +postCheck +preCheck +preConfigure +propagatedBuildInputs +propagatedNativeBuildInputs +shell +src +stdenv +system +testInputs +version ~PATH
Bonus: you can see all the variables set by the nix-shell :)
Now you can leave your project and the environment will be unloaded:
$ cd ..
direnv: unloading
No need to use direnv allow
anymore, the next time you go to your project the environment will be loaded!
More explanation and configuration tweaks can be found in the Direnv wiki [5].
Troubleshooting
When compiling software which links against local files (e.g. when compiling with rust's cargo), you may encounter the following problem:
= note: impure path `/[...]' used in link
This happens due to a specialty in nix: ld
is wrapped in a shell script which refuses to link against files not residing in the nix store, to ensure the purity of builds. Obviously this is not useful when building locally, for example in your home directory. To disable this behavior simply set
NIX_ENFORCE_PURITY=0
in the nix-shell.
No GSettings schemas are installed on the system
When working with gtk, the XDG_DATA_DIRS
must contain a path to the gtk schemas, if not an application may crash with the error above.
For packages we use wrapGAppsHook
in nativeBuildInputs
, however in nix-shell this is not working as expected.
To get your application to work in nix-shell you will need to add the following to your mkShell
expression:
mkShell {
...
buildInputs = [ gtk3 ];
shellHook = ''
export XDG_DATA_DIRS=$GSETTINGS_SCHEMA_PATH
'';
}
This may also called: $GSETTINGS_SCHEMAS_PATH
.
Icons not working
Similar to the Gsettings issue, icons can be added with XDG_DATA_DIRS:
XDG_DATA_DIRS=...:${hicolor-icon-theme}/share:${gnome3.adwaita-icon-theme}/share