Using Nix-Shell for Project-Specific Tools for Emacs

Posted on October 26, 2021 by Richard Goulter
Tags: ,

UPDATE: “Project Specific Tooling in Emacs with Nix Flakes and Direnv” discusses an approach with works with the devShell of flake.nix by making use of direnv.


One feature VSCode has is its “Remote - Containers” extension which allows for developing inside a container.

I found this strange when I first heard it; since every time I’ve docker exec -it’d into a container, I’ve had to then further install tools like vim or a better grep. – Instead, I understand the idea is actually to use the container image as a way of distributing a set of tools to use with the editor.

I liked the idea of project-specific tooling more once I made some use of direnv. direnv enables automatically setting environment variables for the directories you cd through, by sourcing any .envrc files it encounters.

That allows e.g. having a separate directory structure for dev and staging deployments, where the dev/.envrc sets the AWS_PROFILE (or CLOUDSDK_ACTIVE_CONFIG_NAME, etc.) environment variable accordingly. – This reduces the risk of mis-matching commands intended for dev in production. But also increases the convenience of not needing to explicitly change between profiles.

direnv has also has some integration with nix-shell. e.g. https://hardselius.github.io/2020/nix-shell-and-direnv/ – Although Nix is weird, using dotfiles to enable using different versions of tools depending on the directory is a solution as implemented with tools like rbenv (or the more general asdf-vm).

The direnv + nix-shell combo works for command-line shells, and VSCode can have its Docker containers.

Here’s an example of trying the same trick with Emacs.

e.g. for a Terraform file like main.tf:

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 3.0"
    }
  }
}

provider "aws" {
}

variable "ssh_authorized_key" {
  type = string
}

resource "aws_key_pair" "key" {
  key_name_prefix = "ubuntu-vm"
  public_key      = var.ssh_authorized_key
}

resource "aws_security_group" "allow_ssh" {
  name        = "allow_ssh"
  description = "Allow SSH inbound traffic"

  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

data "aws_ami" "ubuntu" {
  most_recent = true

  filter {
    name   = "name"
    values = ["ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*"]
  }

  filter {
    name   = "virtualization-type"
    values = ["hvm"]
  }

  owners = ["099720109477"] # Canonical
}

resource "aws_instance" "ubuntu" {
  ami           = data.aws_ami.ubuntu.id
  instance_type = "t3.small"

  key_name = aws_key_pair.key.key_name

  vpc_security_group_ids = [aws_security_group.allow_ssh.id]

  associate_public_ip_address = true

  root_block_device {
    volume_size = 20
  }
}

output "vm_public_ip" {
  value = aws_instance.ubuntu.public_ip
}

and a shell.nix with contents:

{ pkgs ? import <nixpkgs> {} }:

with pkgs;
mkShell {
  buildInputs = [
    terraform
    terraform-ls
    tflint
  ];
}

(In this case, the shell includes the packages: terraform which is what interprets/runs the main.tf, terraform-ls as the Language Server for LSP, and tflint as the linting tool).

An .envrc can be used to set AWS_PROFILE, AWS_REGION, and TF_VAR_ssh_authorized_key.

To integrate this with Emacs:

Using nix-sandbox for convenience, we can set some variables in a .dir-locals.el file, e.g.:

((terraform-mode . ((eval . (progn
                              (setq-local lsp-terraform-server
                                          `("nix-shell"
                                            "--command"
                                            "terraform-ls serve"
                                            ,(nix-current-sandbox)))))))
 (prog-mode . ((flycheck-command-wrapper-function
                . (lambda (command) (apply 'nix-shell-command (nix-current-sandbox) command)))
               (flycheck-executable-find
                . (lambda (cmd) (nix-executable-find (nix-current-sandbox) cmd))))))

The lsp-terraform-server variable is used by lsp-mode, and the flycheck-command-wrapper-function is for flycheck.

With this, e.g. the terraform-ls from the nix-shell environment can be used for LSP code intelligence, and tflint by flycheck.

I’ve been pretty lazy about figuring out how to setup LSP servers for whatever programming language I’ve been using. Nix doesn’t promise to make that easier. – But what Nix does support is making it easier for someone to make use of a development setup written in Nix (e.g. just running a single “nix-shell” command, rather than following several steps in a blogpost).

I don’t know if project-specific tooling will catch on. I think project-specific tooling looks like it goes well with web applications like replit, gitpod, or GitHub’s codespaces.


Newer post Older post