Automatically Installing Your Emacs Packages
The interactive list-packages
command in modern Emacsen is handy for
finding and trying out new packages, but once we’ve found packages we
want to use, how can we have them automatically installed on all
machines where we use Emacs? There’s
a decent Stack Overflow question on the topic, but I
want to dig into the various answers a bit and provide a slightly
cleaner (IMHO) code snippet.
First let’s define that list of packages we want installed by adding a
defvar
form to our .emacs
:
(defvar my/packages
'(abc-mode
;; ⋮
zygospore))
Now, the obvious part of this problem is using package-installed-p
to check if each named package is installed and then installing the
missing ones. The less obvious part is that we may need to call
package-refresh-contents
to get the package repository data
(especially on the first load) or else we’ll get errors. High-level,
there are four ways I know of to approach this problem:
- Always call
package-refresh-contents
. - Check if
package-archive-contents
is non-nil. - Test if
package-user-dir
exists. - Refresh if any packages are missing.
Number one works buts is extremely inefficient–I don’t want to wait for a package repo fetch every time I start Emacs.
Two and three have a fatal flaw: just because we have some package data doesn’t mean that we have the latest data. So it’s entirely possible that we don’t know about a recent package that the user wants to install–errors again.
The fourth method is the way to go. Surprising no one1, @bbatsov shows up with the right answer. But I do think this code could be a hair cleaner, so here’s my take:
(require 'cl-lib)
(defun my/install-packages ()
"Ensure the packages I use are installed. See `my/packages'."
(interactive)
(let ((missing-packages (cl-remove-if #'package-installed-p my/packages)))
(when missing-packages
(message "Installing %d missing package(s)" (length missing-packages))
(package-refresh-contents)
(mapc #'package-install missing-packages))))
(my/install-packages)
Closing notes:
- The function2 exists so that you can invoke it manually (
M-x my/install-packages RET
) if you’ve changedmy/packages
at runtime. - Keep in mind that re-evaluating
my/packages
after changing it will not do anything because that’s howdefvar
works. Temporarily changedefvar
tosetq
and you’ll be good to go. cl-lib
is required forcl-remove-if
. How Elisp has made it this far without afilter
function, I don’t know.- I’m not entirely sure if
nil
punning is idiomatic elisp or if there’s a more appropriate way to check for empty lists. Anyone know?