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]