Downloading Emacs packages ad-hoc with Nix

[Blog Index]​ [RSS]​ [Prev: Developing Java applications with Emacs using Nix] [Next: "Russian Bullshit" and its variant card games]

Posted on 2024-09-02 in emacs nix .

It's often nice to ad-hoc install packages with package-install, but why accumulate packages in $HOME/.emacs.d when they could simply be installed (and eventually garbage-collected) from the Nix store?

Here is some janky Emacs lisp code I wrote to build and load packages directly from the store. To be clear, I use these only if I want to ad-hoc install packages in my current Emacs session. (If I want to actually use a package in my Emacs configuration, I add it to the extraEmacsPackages attribute of my Emacs nix derivation.)

To get a package, we need to build it from nixpkgs. To do this, here is a function that takes any flake reference to a derivation and runs nix build on it. I added a process sentinel to allow the caller to register a callback (cont).

(defun sid/nix-build (what &optional cont)
  "Build WHAT with `nix build'

Once done, if provided call CONT."
  (declare (indent defun))
  (let ((proc (start-process "nixbuild" "*nix build*"
                             "nix" "build"
                             "--no-link"
                             "--log-format" "raw"
                             "--quiet"
                             "--print-out-paths"
                             what)))
    (set-process-sentinel proc
                          (sid/nix-build-process-sentinel
                           (or cont (lambda ()))))))

(defun sid/nix-build-process-sentinel (cont)
  (lambda (proc _msg)
    (let ((status (process-status proc))
          (exit-status (process-exit-status proc)))
      (cond ((eq status 'signal)
             (error "Nix build failed to run"))
            ((eq status 'exit)
             (if (= exit-status 0)
                 (funcall cont)
               (error "Nix build exited with code %d" exit-status)))
            (t nil)))))

Then, we need to get a list of all things that need to be added to Emacs' load-path. This uses nix path-info to recursively get the path of all dependencies of the Emacs package we're interested in.

A couple of find commands convert the output of nix path-info to the actual Emacs package directories that need to be added to load-path.

(defun sid/nix-get-paths (what)
  "Add all paths from WHAT to `load-path'.

WARNING: janky.

WHAT should be a derivation in flake reference format, like
\"nixpkgs#emacsPackages.helm\"."
  (string-split
   (string-trim
    (shell-command-to-string
     (concat
      "find $(find $(nix path-info --recursive --quiet "
      what
      ") -name site-lisp -type d) -mindepth 2 -maxdepth 2 -name \\* -type d")))
   "\n"))

Finally, put it all together. Take an Emacs package name, build it, and add all of its paths to load-path in the callback.

(defun sid/nix-load (name &optional package-name)
  "Load NAME from nixpkgs, specifically nixpkgs#emacsPackages.NAME.

If specified, PACKAGE-NAME can override NAME if the Nix package name
isn't the same as the emacs package name."
  (let ((pkg (or package-name (concat "nixpkgs#emacsPackages." (symbol-name name)))))
    (sid/nix-build pkg
      (lambda ()
        (mapcar (lambda (x) (add-to-list 'load-path x))
                (sid/nix-get-paths pkg))
        (require name))))
  name)

(provide 'nix-load)

Test it out with a package that isn't already installed:

(sid/nix-load 'helm)

This is still asynchronous, so any code that expects the package to be loaded needs to be wrapped in with-eval-after-load like this:

(with-eval-after-load 'helm
  ;; do something with helm ...
  (message "Helm loaded! Hooray!"))

[Blog Index]​ [RSS]​ [Prev: Developing Java applications with Emacs using Nix] [Next: "Russian Bullshit" and its variant card games]