direnv is a useful tool to automatically set environment variables per project and it also has a nix integration!

I'm used to define an .envrc file in my projects which would expose some tools to work with the project.

For example I'm using zola to write this blog. I don't need the zola command in my user environment all the time it's only useful when working on the blog. With direnv I could set this up with two files in the project root directory:

shell.nix:

let
  pkgs = import <nixpkgs> {};
in
  pkgs.mkShell {
    buildInputs = [
        pkgs.zola
    ];
  }

.envrc:

use_nix

After running direnv allow this would happen when going in the project directory:

$ zola
The program ‘zola’ is currently not installed. You can install it by typing:
  nix-env -iA nixos.zola
$ cd blog
direnv: loading .envrc
direnv: export +AR +AS +CC +CONFIG_SHELL +CXX +HOST_PATH +IN_NIX_SHELL +LD +NIX_BINTOOLS +NIX_BINTOOLS_WRAPPER_x86_64_unknown_linux_gnu_TARGET_HOST +NIX_BUILD_CORES +NIX_BUILD_TOP +NIX_CC +NIX_CC_WRAPPER_x86_64_unknown_linux_gnu_TARGET_HOST +NIX_ENFORCE_NO_NATIVE +NIX_HARDENING_ENABLE +NIX_INDENT_MAKE +NIX_LDFLAGS +NIX_STORE +NM +OBJCOPY +OBJDUMP +RANLIB +READELF +SIZE +SOURCE_DATE_EPOCH +STRINGS +STRIP +TEMP +TEMPDIR +TMP +TMPDIR +buildInputs +builder +configureFlags +depsBuildBuild +depsBuildBuildPropagated +depsBuildTarget +depsBuildTargetPropagated +depsHostHost +depsHostHostPropagated +depsTargetTarget +depsTargetTargetPropagated +doCheck +doInstallCheck +name +nativeBuildInputs +nobuildPhase +out +outputs +patches +phases +propagatedBuildInputs +propagatedNativeBuildInputs +shell +shellHook +stdenv +strictDeps +system ~PATH
$ zola
zola 0.9.0
Vincent Prouillet <hello@vincentprouillet.com>
...

We can see that the $PATH was updated so that zola is now available. That's great but a lot of other other environment variables that we don't care about were defined. Indeed direnv uses nix-shell behind the scenes to populate the environment but nix-shell is designed to debug the build of derivations not just bring some tools in the $PATH. This problem is discussed here.

It's also not very fast, almost half a second on my machine:

$ cd blog
direnv: loading .envrc

real  0m0.442s
user  0m0.333s
sys   0m0.068s
direnv: export +AR +AS +CC +CONFIG_SHELL +CXX +HOST_PATH +IN_NIX_SHELL +LD +NIX_BINTOOLS +NIX_BINTOOLS_WRAPPER_x86_64_unknown_linux_gnu_TARGET_HOST +NIX_BUILD_CORES +NIX_BUILD_TOP +NIX_CC +NIX_CC_WRAPPER_x86_64_unknown_linux_gnu_TARGET_HOST +NIX_ENFORCE_NO_NATIVE +NIX_HARDENING_ENABLE +NIX_INDENT_MAKE +NIX_LDFLAGS +NIX_STORE +NM +OBJCOPY +OBJDUMP +RANLIB +READELF +SIZE +SOURCE_DATE_EPOCH +STRINGS +STRIP +TEMP +TEMPDIR +TMP +TMPDIR +buildInputs +builder +configureFlags +depsBuildBuild +depsBuildBuildPropagated +depsBuildTarget +depsBuildTargetPropagated +depsHostHost +depsHostHostPropagated +depsTargetTarget +depsTargetTargetPropagated +doCheck +doInstallCheck +name +nativeBuildInputs +nobuildPhase +out +outputs +patches +phases +propagatedBuildInputs +propagatedNativeBuildInputs +shell +shellHook +stdenv +strictDeps +system ~PATH

So for this use case I thought of an alternative approach which involves nix run. Since nix v2 nix run is available. It doesn't replace nix-shell completely but it could be used in this situation.

Running nix run nixpkgs.zola would open a new shell an make zola available in $PATH. By checking the environment we can confirm it does only that:

$ env | sort > env.current
$ nix run nixpkgs.zola
$ env | sort > env.after
$ diff -u env.current env.after
-PATH=/home/eon/bin:/run/wrappers/bin:/home/eon/.nix-profile/bin:/etc/profiles/per-user/eon/bin:/nix/var/nix/profiles/default/bin:/run/current-system/sw/bin
+PATH=/nix/store/d4wzwmy02r2d4jpk02ha6xi2nm5vk0li-zola-0.9.0/bin:/home/eon/bin:/run/wrappers/bin:/home/eon/.nix-profile/bin:/etc/profiles/per-user/eon/bin:/nix/var/nix/profiles/default/bin:/run/current-system/sw/bin
-SHLVL=4
+SHLVL=5

Next how we could integrate this with direnv is quite trivial. By default nix run spawns a new shell but it's possible to specify a different command. In this case we just want the $PATH variable exported by nix run. The .envrc file contains now (and of course we can get rid of the shell.nix file):

export $(nix run nixpkgs.zola -c env | grep ^PATH)

This run a bit faster that the nix-shell version:

$ cd blog
direnv: loading .envrc

real  0m0.313s
user  0m0.256s
sys   0m0.027s
direnv: export ~PATH

And doesn't pollute the environment!