Using Nix-Shell for Project-Specific Tools for Emacs
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"
= "~> 3.0"
version
}
}
}
provider "aws" {
}
variable "ssh_authorized_key" {
= string
type
}
resource "aws_key_pair" "key" {
= "ubuntu-vm"
key_name_prefix = var.ssh_authorized_key
public_key
}
resource "aws_security_group" "allow_ssh" {
= "allow_ssh"
name = "Allow SSH inbound traffic"
description
ingress {= 22
from_port = 22
to_port = "tcp"
protocol = ["0.0.0.0/0"]
cidr_blocks
}
}
data "aws_ami" "ubuntu" {
= true
most_recent
filter {= "name"
name values = ["ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*"]
}
filter {= "virtualization-type"
name values = ["hvm"]
}
= ["099720109477"] # Canonical
owners
}
resource "aws_instance" "ubuntu" {
= data.aws_ami.ubuntu.id
ami = "t3.small"
instance_type
= aws_key_pair.key.key_name
key_name
= [aws_security_group.allow_ssh.id]
vpc_security_group_ids
= true
associate_public_ip_address
root_block_device {= 20
volume_size
}
}
output "vm_public_ip" {
= aws_instance.ubuntu.public_ip
value }
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.