image of me

An Emacs + Nix Workflow

2020-07-29

Table of Contents


This is a post which goes through my current workflow, and how to set it up for yourself. I use emacs as the editor, but I think it's probably not essential - as long as what you're using has good direnv support then it should do (give emacs a try though - it's genius).

My workflow uses:

It's pretty distro-agnostic, as nix can be installed as a complimentary package manager pretty much anywhere.

Nix

Nix is both (confusingly) a programming langauge and a package manager, with the former being used to control the latter. The main draw of the package manager is that it focusses heavily on reproducible builds. Every thing that the package manager installs is stored in a separate folder under the nix directory, with a name given by its own hash. Nix then symlinks the packages you need into the expected folders and modifies your environment variables, allowing you to add and remove packages incredibly easily. I always find that when I uninstall some package with a traditional package manager, it'll leave bits of residue all over the system. This doesn't happen with nix: to remove a package, its corresponding folder just needs to be deleted. The fact that each package gets installed to a unique folder also means that you can have multiple conflicting versions existing on the same system, and only symlink each when you need it.

With a senile style of package manager, you'll punch in a list of commands "update this, install that...", the package manager will follow along and hopefully you'll get a system with the set of packages that you wanted. With nix, you instead specify the packages you want installed in a file (written in the nix language), and then you run the package manager and it'll get the system to that state. Classic package managers are like writing a recipe and then following it through yourself; nix is like writing a recipe and getting a computer to do the rest for you. I make less mistakes this way.

The useful thing about your whole environment being symlinked is that you can switch between different environments really quickly. Nix has a command for this: nix-shell. Running nix-shell --packages vim python3 will download vim and the python 3 interpreter and stick you into an environment with them available. You just need to exit to get out of the nix-shell, and back into your previous environment. If you don't want to have to type nix-shell -p massive list of packages each time you want your specific environment, you can just create a shell.nix file. This should contain the specific packages you need, commands you want to run when it starts up, etc... You can then just go into a directory with a shell.nix file, type nix-shell and a lovely reproducible development environment will be available to you.

Here's an example shell.nix which'll put you into an environment with emacs and python 3 with scipy, numpy and matplotlib installed.

{ pkgs ? import <nixpkgs> { } }:
pkgs.mkShell {
    buildInputs = [
        (pkgs.python3.withPackages (ps: with ps; [ scipy numpy matplotlib ]))
        pkgs.emacs
    ];
}

There's plenty of guides online3 about how to create files for nix-shell, so I won't go over it here.

Now, I'm lazy and don't want to have to type a command every time I enter and exit a folder to load its environment. How do I fix this?

Direnv

Direnv is a tool which allows you to load and unload environments automatically when you enter and exit a directory. You just create a .envrc file in whatever folder you're rigging up, and put whatever shell commands you need to be run in there to set up your environment.

There's an emacs package called emacs-direnv which hooks up the direnv environment to be entered and exited when your buffer goes in and out of the directory. It's nice, but if you try to hook it up with nix to run nix-shell as-is, then you'll notice it's a bit too slow...

Lorri

Lorri is a daemon which allows direnv to load and unload nix shells way faster than just using nix-shell. It does this by having a daemon watch your shell.nix files, and building and caching them automatically whenever something changes. It's really easy to install, and has a nice little shortcut to setting up a direnv folder. You just use the command lorri init, and it'll put a shell.nix and .envrc file in the folder, just requiring you to specify what software you want.

(It's written in a good language too.)


With all of these packages together, you're able to open up buffers in emacs with all of the environment specified, and from my experience (mostly with python, haskell, rust) it's pretty smooth. Have fun.