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-contentsis non-nil. - Test if
package-user-direxists. - 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/packagesat runtime. - Keep in mind that re-evaluating
my/packagesafter changing it will not do anything because that’s howdefvarworks. Temporarily changedefvartosetqand you’ll be good to go. cl-libis required forcl-remove-if. How Elisp has made it this far without afilterfunction, I don’t know.- I’m not entirely sure if
nilpunning is idiomatic elisp or if there’s a more appropriate way to check for empty lists. Anyone know?