My usage of Nix, and Lorri + Direnv

I recently wrote about me transitioning my main working environment to run on NixOS. I've used NixOS on my main workstation and Nix atop Ubuntu on WSL2 on my laptop for ~5 months now and I feel I've really gotten into the flow with it on a handful of client projects.

The coolest bit of the setup is Nix/NixOS agnostic: lorri and direnv. At face value, they're usecase is pretty basic and we will dive into how you're meant to use them on projects, but using them actually enables some pretty cool functionality I've been trying to implement for awhile now.

What is Lorri and Direnv?

Nix provides a really cool utility built in called nix-shell. Basically, invoking nix-shell starts a new interactive shell after evaluating a given Nix expression (which by default is assumed to be ./shell.nix, relative to the CWD). This means that you can write a Nix expression detailing what packages you want to install, what environment variables to set, and automatically set/build/enable them within the context of the interactive shell.

This is already extremely powerful as you can declarative manage software versions etc, but Lorri + Direnv make this even nicer to use!

direnv is a super neat project which provides a shell hook that allows your shell to automatically set environment variables defined within a .envrc file (again, relative the the CWD) whenever you change directories. A basic .envrc is very simple and can look like the following:

export SOME_ENV_VARIABLE="test"

A major win here is that it's super fast, supports most popular shells (including some more esoteric ones), is language agnostic, and is pretty extensible.

lorri is essentially an extension of nix-shell built atop direnv. As soon as you change directory, if the new CWD has a shell.nix and an appropriate .envrc (autogenerated by running lorri init), the shell.nix is immediately evaluated. Lorri improves upon nix-shell in a bunch of ways as well, w.r.t caching of built derivations so it is also super quick.

General usage

Once you have lorri and direnv set up, the most basic use case in my opinion is replacing something like asdf which does much of the same stuff:

Since lorri is built with Nix however, we get the following instead:

For example, here is an example shell.nix I needed to bootstrap an Elixir project which set some private credentials and needed Docker, GCS utilities, a DB client, and Minikube installed to run:

let
  pkgs = import <nixpkgs> {};
in
  g_sec_user = <REDACTED>;
  g_sec_password = <REDACTED>;

  pkgs.mkShell {
    buildInputs = [
      pkgs.elixir_1_10
      pkgs.nodejs-10_x
      pkgs.yarn
      pkgs.inotify-tools
      pkgs.openssl
      pkgs.kubectl
      pkgs.jq
      pkgs.google-cloud-sdk
      pkgs.minikube
      pkgs.kubernetes-helm
    ];
  }

Once this file is created, everything automagically works when entering that directory. Changes to the shell.nix are also automatically tracked so you don't need to do anything fancy: your shell.nix is your single source of truth.

Another advantage is that once your project has a shell.nix checked into it's repository, anyone running Nix/NixOS can start hacking away by just running shell.nix, but if you they Direnv + Lorri, they just have to enter the directory and stuff just works ๐ŸŽ‰

Usage for Nix unfriendly projects

I'm an Erlang/Elixir consultant, and as a result of this, I have the opportunity to work on a lot of projects for a lot of different clients. I also know that a relatively esoteric package manager (or worse, an entirely different Linux distribution) is a hard sell.

Thankfully though, even in environments where you can't get away with commiting any of the lorri/direnv artefacts into the core repository of a project, you can get away with using lorri + direnv!

Because this entire workflow is dependent on directory structure, my workstation typically has a structure like this:

/home/chris/git
โ”œโ”€โ”€ esl
โ”‚ย ย  โ”œโ”€โ”€ shell.nix
โ”‚ย ย  โ”œโ”€โ”€ internal_project_1
โ”‚ย ย  โ””โ”€โ”€ internal_project_2
โ”‚    ย ย  โ””โ”€โ”€ shell.nix
โ”œโ”€โ”€ client_1
โ”‚ย ย  โ”œโ”€โ”€ shell.nix
โ”‚ย ย  โ”œโ”€โ”€ client_project_1
โ”‚ย ย  โ”œโ”€โ”€ client_project_2
โ”‚ย ย  โ”œโ”€โ”€ client_project_3
โ”‚ย ย  โ””โ”€โ”€ client_project_4
โ”œโ”€โ”€ client_2
โ”‚ย ย  โ”œโ”€โ”€ shell.nix
โ”‚ย ย  โ””โ”€โ”€ client_project_1
โ””โ”€โ”€ vereis
  ย  โ”œโ”€โ”€ shell.nix
    โ”œโ”€โ”€ blog
    โ”‚ย ย  โ””โ”€โ”€ shell.nix
    โ”œโ”€โ”€ build_you_a_telemetry
    โ”‚ย ย  โ””โ”€โ”€ shell.nix
    โ”œโ”€โ”€ horde
    โ”œโ”€โ”€ httpoison
    โ””โ”€โ”€ nixos

If I'm not allowed to add direnv/lorri artefacts, I can simply put them a directory above the repository and I still get all of the benefits ๐Ÿ˜„ You need to add the source_up to any child .envrc files in order to also execute their parent but otherwise it works as expected.

This means that for I can install packages / handle dependencies on a project by project basis, or a client by client bases. Because the effects of executing these Nix expressions is cumulative, you can even do it on a client by client basis, except for a given project for that client.

As Lorri/Direnv lets me install dependencies and set environment variables, this setup is also perfect for a seamless dev credential manager solution. For example, assuming a client has it's own npm registry for JavaScript dependencies, you can simple do the following:

let
  pkgs = import <nixpkgs> {};
in
pkgs.mkShell {
  buildInputs = [
    pkgs.nodejs-10_x
  ];

  npm_config_user_config = toString ./. + "/.npmrc";
}

Where /home/chris/client_1/.npmrc contains:

registry=<REDACTED>
email=<REDACTED>
init.author.name="Chris Bailey"
init.author.email=<REDACTED>
always-auth=true
_auth=<REDACTED>

Notice that esl, client_1, client_2, and vereis all have a top level shell.nix. This is also perhaps a slight abuse of direnv/lorri but the end effect is that I'm able to treat these different directories as completely separate development domains. Each of these top level shell.nix files also set environment variables to set my git username and email. This means that I no longer need to faff around with setting up each and every git repo I clone: if I'm in /home/chris/git/vereis, I'm going to be working as @vereis, if I'm in /home/chris/git/esl/ I'll be @cbaileyesl etc.

Conclusion

I hope this small write-up has been helpful ๐Ÿ˜„ I genuinely think that the Nix ecosystem offers a lot of advantages, especially for developers. Tools like nix-shell, lorri, and direnv are super helpful in streamlining the development process.

Of course, tools like asdf, dotenv and others exist which allow you to emulate all of what I've described, but the true advantage of Nix/NixOS is that the configuration is very declarative and built into the core experience. Any nix user can do exactly what I've described, even if they're not running lorri or direnv simply by virtue that it's all built upon the first-class nix-shell.

I hope if you're not a nix user, this has piqued your interests a little bit, and if you are a nix user, this will help you jump a few of the small roadbumps when trying to approach development leveraging all the niceties nix provides you.


Return to Posts โ†’