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:

  1. Always call package-refresh-contents.
  2. Check if package-archive-contents is non-nil.
  3. Test if package-user-dir exists.
  4. 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 changed my/packages at runtime.
  • Keep in mind that re-evaluating my/packages after changing it will not do anything because that’s how defvar works. Temporarily change defvar to setq and you’ll be good to go.
  • cl-lib is required for cl-remove-if. How Elisp has made it this far without a filter 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?

  1. Bozhidar Batsov is killing it in the Emacs community right now and you should probably be following him. ↩︎

  2. Rather than including what is currently the function body at the top level of the .emacs file. ↩︎