Project Specific Tooling in Emacs with Nix Flakes and Direnv

Posted on March 21, 2022 by Richard Goulter
Tags:

One use-case for nix is to describe the tools/dependencies used on a per-project basis. – This is similar to VSCode’s Remote Containers, but without the containers.

We can achieve this in Emacs by leveraging direnv, nix-direnv, and emacs-direnv.

direnv provides a neat way of specifying environment variables in a per-directory basis.

Since direnv runs arbitrary bash code in the .envrc files, it’s possible to use direnv to automatically load programs specified in a nix file into the enviroment.

Using nix-direnv

With the new flake-aware nix cli commands, a flake.nix file is preferred over shell.nix or default.nix. The nix-direnv project integrates direnv with nix flakes.

Example .envrc and flake.nix files are given in nix-direnv’s template/ directory. These can also be generated by running:

nix flake new -t github:nix-community/nix-direnv .

(and direnv allow to grant direnv permission to run the file).

But for the sake of saving a click, it’s something like:

if ! has nix_direnv_version || ! nix_direnv_version 1.6.0; then
    source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/1.6.0/direnvrc" "sha256-FqqbUyxL8MZdXe5LkMgtNo95raZFbegFpl5k2+PrCow="
fi
use flake
{
  description = "A basic flake with a shell";
  inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
  inputs.flake-utils.url = "github:numtide/flake-utils";

  outputs = { self, nixpkgs, flake-utils }:
    flake-utils.lib.eachDefaultSystem (system: let
      pkgs = nixpkgs.legacyPackages.${system};
    in {
      devShell = pkgs.mkShell {
        nativeBuildInputs = [ pkgs.bashInteractive ];
        buildInputs = [ ];
      };
    });
}

emacs-direnv is a package which will load the environment from direnv .envrc files.

Since emacs-direnv loads the .envrc file, and the .envrc file loads the environment with the devShell from the flake.nix, Emacs will use the programs from the devShell. (As can be verified by evaluating (getenv "PATH") or (executable-find "bash"), or whatever).

With recent versions of direnv, the .envrc can be as simple as:

use flake

and I often find it useful to refer to a devShell output of a flake elsewhere:

use flake github:rgoulter/nix-user-repository#python_3_10

Example: Go-lang

e.g. for Go-lang, we want go and gopls, so our devShell is defined in the flake.nix as:

devShell = pkgs.mkShell {
  packages = with pkgs; [
    go
    gopls
  ];
};

Example: Terraform

e.g. for Terraform:

devShell = pkgs.mkShell {
  packages = with pkgs; [
    terraform
    terraform-ls
    tflint
  ];
};

But, using terraform-ls instead of terraform-lsp is a bit more involved. In order to get the LSP working, I needed to fill .dir-locals.el with:

((terraform-mode . ((eval . (progn
                              (direnv-update-environment)
                              (setq-local lsp-terraform-server `("terraform-ls" "serve")))))))

Downsides

And obviously it takes a bit of effort to write the above.


Newer post Older post