Ad-Hoc Emacs Packages with Nix

You can use Nix as a package manager for Emacs, like so:

{
  home-manager.users.eudoxia = {
    programs.emacs = {
      enable = true;
      extraPackages =
        epkgs: with epkgs; [
          magit
          rust-mode
          treemacs
          # and so on
        ];
    };
  };
}

Today I learned you can also use it to create ad-hoc packages for things not in MELPA or nixpkgs.

The other day I wanted to get back into Inform 7, naturally the first stack frame of the yak shave was to look for an Emacs mode. inform7-mode exists, but isn’t packaged anywhere. So I had to vendor it in.

You can use git submodules for this, but I have an irrational aversion to submodules. Instead I did something far worse: I wrote a Makefile to download the .el from GitHub, and used home-manager to copy it into my .emacs.d. Which is nasty. And of course this only works for small, single-file packages. And, on top of that: whatever dependencies your vendored packages need have to be listed in extraPackages, which confuses the packages you want, with the transitive dependencies of your vendored packages.

I felt like the orange juice bit from The Simpsons. There must be a better way!

And there is. With some help from Claude, I wrote this:

let
  customPackages = {
    inform7-mode = pkgs.emacsPackages.trivialBuild {
      pname = "inform7-mode";
      version = "unstable";
      src = pkgs.fetchFromGitHub {
        owner = "alexispurslane";
        repo = "inform7-mode";
        rev = "f99e534768c816ec038f34126f88d816c2f7d9ff";
        sha256 = "sha256-r9Zzd8Ro3p+Bae11bf1WIeVWkbmg17RKLDqG4UcFT1o=";
      };
      packageRequires = with pkgs.emacsPackages; [
        s
      ];
    };
  };
in
{
  home-manager.users.eudoxia = {
    programs.emacs = {
      enable = true;
      extraPackages =
        epkgs: with epkgs; [
          customPackages.inform7-mode
          # ...
        ];
    };
  };
}

Nix takes care of everything: commit pinning, security (with the SHA-256 hash), dependencies for custom packages. And it works wonderfully.

Armed with a new hammer, I set out to drive some nails.

cabal-mode

Today I created a tiny Haskell project, and when I opened the .cabal file, noticed it had no syntax highlighting. I was surprised to find there’s no cabal-mode in MELPA. But coincidentally, someone started working on this literally three weeks ago! So I wrote a small expression to package this new cabal-mode:

cabal-mode = pkgs.emacsPackages.trivialBuild {
  pname = "cabal-mode";
  version = "unstable";
  src = pkgs.fetchFromGitHub {
    owner = "webdevred";
    repo = "cabal-mode";
    rev = "083a777e09bdb5a8d8d69862d44f13078664091f";
    sha256 = "sha256-c5dUsnEx+0uXFzxQLMnhiP8Gvwedzvq0F0BA+beBkmI=";
  };
  packageRequires = [ ];
};

xcompose-mode

A few weeks back I switched from macOS to Linux, and since I’m stuck on X11 because of stumpwm, I’m using XCompose to define keybindings for entering dashes, smart quotes etc. It bothered me slightly that my .XCompose file didn’t have syntax highlighting. I found xcompose-mode.el in kragen’s xcompose repo, but it’s slightly broken (it’s missing a provide call at the end). I started thinking how hard it would be to write a Nix expression to modify the source after fetching, when I found that Thomas Voss hosts a patched version here. Which made this very simple:

xcompose-mode = pkgs.emacsPackages.trivialBuild {
  pname = "xcompose-mode";
  version = "unstable";
  src = pkgs.fetchgit {
    url = "git://git.thomasvoss.com/xcompose-mode.git";
    rev = "aeb03f9144e39c882ca6c5c61b9ed1300a2a12ee";
    sha256 = "sha256-lPapwSJKG+noINmT1G5jNyUZs5VykMOSKJIbQxBWLEA=";
  };
  packageRequires = [ ];
};

eat

Somehow the version of eat in nixpkgs unstable was missing the configuration option to use a custom shell. Since I want to use nu instead of bash, I had to package this myself from the latest commit:

eat = pkgs.emacsPackages.trivialBuild {
  pname = "eat";
  version = "unstable";
  src = pkgs.fetchgit {
    url = "https://codeberg.org/akib/emacs-eat.git";
    rev = "c8d54d649872bfe7b2b9f49ae5c2addbf12d3b99";
    sha256 = "sha256-9xG2rMlaMFY77JzUQ3JFrc7XKILZSL8TbP/BkzvBvMk=";
  };
  packageRequires = with pkgs.emacsPackages; [
    compat
  ];
};

lean4-mode

I started reading Functional Programming in Lean recently, and while there is a lean4-mode, it’s not packaged anywhere. This only required a slight deviation from the pattern: when I opened a .lean file I got an error about a missing JSON file, consulting the README for lean4-mode, it says:

If you use a source-based package-manager (e.g. package-vc.el, Straight or Elpaca), then make sure to list the "data" directory in your Lean4-Mode package recipe.

To do this I had to use melpaBuild rather than trivialBuild:

lean4-mode = pkgs.emacsPackages.melpaBuild {
  pname = "lean4-mode";
  version = "1.1.2";
  src = pkgs.fetchFromGitHub {
    owner = "leanprover-community";
    repo = "lean4-mode";
    rev = "1388f9d1429e38a39ab913c6daae55f6ce799479";
    sha256 = "sha256-6XFcyqSTx1CwNWqQvIc25cuQMwh3YXnbgr5cDiOCxBk=";
  };
  packageRequires = with pkgs.emacsPackages; [
    dash
    lsp-mode
    magit-section
  ];
  files = ''("*.el" "data")'';
};