Bundling Emacs packages with Nix

Aside from managing an Emacs installation, Nix can also bundle Emacs packages. For example, to produce a development shell that has Emacs available with Evil mode installed, use emacsWithPackages. In this example flake, the build inputs include a version of Emacs based on emacs-nox with the Evil package added:

{
  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
    flake-utils.url = "github:numtide/flake-utils";
  };

  outputs = {
    nixpkgs,
    flake-utils,
    ...
  }:
    flake-utils.lib.eachDefaultSystem (system: let
      pkgs = import nixpkgs {inherit system;};
    in {
      devShells.default = pkgs.mkShell {
        buildInputs = with pkgs; [
          ((emacsPackagesFor emacs-nox).emacsWithPackages (epkgs: [
            epkgs.evil
          ]))
        ];
      };
    });
}

With the flake in place, a development shell is started by running nix develop, which opens a shell with Emacs installed and available. To start Emacs directly, without having to start a sub shell first, use the --command flag:

nix develop --command emacs

Within this version of Emacs, the packages defined in the flake’s build inputs are available. This allows for reproducible setups of local development environments, or when using Emacs for batch processing, for example.

Nixpkgs automatically creates derivations based on the recipes available in Elpa, Elpa-devel, NonGNU Elpa and Melpa, which means most packages are available in Nix’s emacsPackage set1.

An Emacs package derivation

Emacs packages can be used even if they’re not available through Nixpkgs by writing a derivation.

For example, the ox-html-markdown-style-footnotes package is not available through any package manager, but it is useful for generating HTML from Org documents through batch processing. To bundle it with Emacs, write a derivation:

{ lib, fetchurl, trivialBuild }:

trivialBuild {
  pname = "ox-html-markdown-style-footnotes";
  version = "0.3.0";

  src = fetchurl {
    url = "https://raw.githubusercontent.com/jeffkreeftmeijer/ox-html-markdown-style-footnotes.el/8351691b28f79fc40b90eea238c1f1ec5b0ff457/ox-html-markdown-style-footnotes.el";
    sha256 = "sha256-JY0xIIJa2sPfCVXpwCdoUAM2UFk+Iih7M6E8q4GwZPA=";
  };

  meta = with lib; {
    description = "Markdown-style footnotes for ox-html.el";
    homepage = "https://jeffkreeftmeijer.com/ox-html-markdown-style-footnotes/";
    license = licenses.gpl3;
    platforms = platforms.all;
  };
}

The derivation only sets some general information about the package, like the name, file URL and fingerprint2. The trivialBuild function takes care of the rest.

This example uses the fetchurl fetcher, which could be swapped out for fetchFromGitHub in this specific scenario.3 However, with this package being contained in a single file, the simplest option will suffice. Also, an advantage of using fetchurl is that it can fetch from anywhere.4

Finally, update the flake to include the newly-added derivation5:

{
  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
    flake-utils.url = "github:numtide/flake-utils";
  };

  outputs = { nixpkgs, flake-utils, ... } :
  flake-utils.lib.eachDefaultSystem (system:
  let
    pkgs = import nixpkgs { inherit system; };
  in
  {
    devShells.default = pkgs.mkShell {
      buildInputs = with pkgs; [
        ((emacsPackagesFor emacs-nox).emacsWithPackages(epkgs: [
          epkgs.evil
          (epkgs.callPackage ./ox-html-markdown-style-footnotes.nix {})
        ]))
      ];
    };
  });
}

  1. To determine if an Emacs package is available through Nixpkgs, use search.nixos.org, or the nix search command:

    nix search nixpkgs emacsPackages.evil
    
    * legacyPackages.aarch64-darwin.emacsPackages.evil (20231106.1213)
    
    * legacyPackages.aarch64-darwin.emacsPackages.evil-anzu (20220911.1939)
    
    * legacyPackages.aarch64-darwin.emacsPackages.evil-args (20220125.1626)
    
    * legacyPackages.aarch64-darwin.emacsPackages.evil-avy (20150908.748)
    
    ...
    
    ↩︎
  2. To get the sha256 value, simply omit it when running for the first time and take the correct value from the output:

    warning: found empty hash, assuming 'sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA='
    error: hash mismatch in fixed-output derivation '/nix/store/144z7a0c6jckqrkrmdnaqdfwi37vqs8m-ox-html-markdown-style-footnotes.el.drv':
             specified: sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
                got:    sha256-S+lzFpGY44OgXAeM9Qzdhvceh8DvvOFiw5tgXoXDrsQ=
    
    ↩︎
  3. The fetchFromGitHub helper is a convenience function that removes some of the boilerplate from fetchurl when fetching from GitHub.

    { lib, fetchFromGitHub, trivialBuild }:
    
    trivialBuild {
      pname = "ox-html-markdown-style-footnotes";
      version = "0.2.0";
    
      src = fetchFromGitHub {
        owner = "jeffkreeftmeijer";
        repo = "ox-html-markdown-style-footnotes.el";
        rev = "8351691b28f79fc40b90eea238c1f1ec5b0ff457";
        sha256 = "sha256-JY0xIIJa2sPfCVXpwCdoUAM2UFk+Iih7M6E8q4GwZPA=";
      };
    
      meta = with lib; {
        description = "Markdown-style footnotes for ox-html.el";
        homepage = "https://jeffkreeftmeijer.com/ox-html-markdown-style-footnotes/";
        license = licenses.gpl3;
        platforms = platforms.all;
      };
    }
    
    ↩︎
  4. For example, nixpkgs uses the elpaBuild helper to include Emacs packages into the registry. To get a different version of a package from GNU Elpa, combine it with fetchurl:

    ((emacsPackagesFor emacs-nox).emacsWithPackages (epkgs: [
      (epkgs.elpaBuild {
        pname = "ef-themes";
        ename = "ef-themes";
        version = "1.11.0";
        src = fetchurl {
            url = "https://elpa.gnu.org/packages/ef-themes-1.11.0.tar";
            sha256 = "086d2fmgzgnjil97zjn2i0ii81dq9l8rr859j0kyaxiy83r27rqb";
        };
      })
    ]))
    
    ↩︎
  5. In a pinch, the derivation can also be placed directly in the flake, as it’s just a function:

    {
      inputs = {
        nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
        flake-utils.url = "github:numtide/flake-utils";
      };
    
      outputs = { nixpkgs, flake-utils, ... } :
      flake-utils.lib.eachDefaultSystem (system:
      let
        pkgs = import nixpkgs { inherit system; };
      in
      {
        devShells.default = pkgs.mkShell {
          buildInputs = with pkgs; [
            ((emacsPackagesFor emacs-nox).emacsWithPackages(epkgs: [
              epkgs.evil
              (epkgs.trivialBuild {
                pname = "ox-html-markdown-style-footnotes";
                version = "0.3.0";
    
                src = fetchurl {
                  url = "https://raw.githubusercontent.com/jeffkreeftmeijer/ox-html-markdown-style-footnotes.el/8351691b28f79fc40b90eea238c1f1ec5b0ff457/ox-html-markdown-style-footnotes.el";
                  sha256 = "sha256-JY0xIIJa2sPfCVXpwCdoUAM2UFk+Iih7M6E8q4GwZPA=";
                };
    
                meta = with lib; {
                  description = "Markdown-style footnotes for ox-html.el";
                  homepage = "https://jeffkreeftmeijer.com/ox-html-markdown-style-footnotes/";
                  license = licenses.gpl3;
                  platforms = platforms.all;
                };
              })
            ]))
          ];
        };
      });
    }
    
    ↩︎