(or emacs irrelevant)

Emacs package management

Lately, I've been spending some time to automate and publish my Emacs config. Being able to quickly reproduce your config has many advantages, the main one being that you no longer have to spend time to make your config reproducible.

Happily, most of my config is already published in many packages, I only have to figure out a nice layer to glue them together. Below, I'll show some automation for the packages managed by package.el.

Step 1: get the main directory

This is an important step that many other peoples' configs miss, even the ones that are designed to be distributed. You can't just assume that the config will be located in ~/.emacs.d and rely on Emacs defaults. Instead, it's nice to be able to clone the config into a random directory and launch an Emacs from there, without messing with the currently installed Emacs.

It's also useful for having multiple repositories for different versions of Emacs. ELPA packages are byte-compiled, and the byte code can be incompatible between versions (for instance, 24.3 and 24.4). Having two independent checkouts with ELPA directory auto-generated really helps in that case.

So here is the code to get the main directory and define an ELPA directory with respect to that:

(defconst emacs-d
  (file-name-directory
   (file-chase-links load-file-name))
  "The giant turtle on which the world rests.")

(setq package-user-dir
      (expand-file-name "elpa" emacs-d))

Step 2: decide what you like

Next, I initialize the package and define some of the packages that I like, omitting the dependencies that they bring. Note that the code of the whole post is stored in a separate file packages.el that is not intended to be loaded on start up, so it's fine to call package-refresh-contents here:

(package-initialize)
(setq package-archives
      '(("melpa" . "http://melpa.milkbox.net/packages/")
        ("gnu" . "http://elpa.gnu.org/packages/")))
(package-refresh-contents)

(defconst ora-packages
  '(auto-compile auto-yasnippet ace-link ace-window
    company eclipse-theme flx-ido function-args
    headlong ido-occasional ido-vertical-mode lispy
    magit smex swiper use-package guide-key
    powerline projectile slime cider worf
    org-download make-it-so ukrainian-holidays
    netherlands-holidays j-mode)
  "List of packages that I like.")

Step 3: install and upgrade

The install step is pretty straightforward: install a package unless it's already installed. I tried to do something fancier for the upgrade, but in the end it was much more simple to just call the interactive interface. The last two lines are basically equivalent to pressing Uxy interactively:

;; install required
(dolist (package ora-packages)
  (unless (package-installed-p package)
    (package-install package)))

;; upgrade installed
(save-window-excursion
  (package-list-packages t)
  (package-menu-mark-upgrades)
  (package-menu-execute t))

Step 4: make it callable

Finally, I just create a Makefile with the following contents:

emacs ?= emacs
upgrade:
    $(emacs) -batch -l packages.el

run:
    $(emacs) -Q -l init.el

up: upgrade run

Thanks to the first line, I can issue stuff like this on the shell:

emacs=emacs24 make up

This will use the emacs24 executable, instead of whatever emacs points to. Since the up target depends on upgrade and run targets, they will be executed in that order:

  • the upgrade will install / upgrade all packages in a non-interactive Emacs
  • the run target will start an interactive Emacs with already updated packages

I really like putting stuff in Makefiles, since they are very flexible, yet so easy to call. In the very same Makefile, I have a profile target from the post on profiling Emacs start up. I also wrote two packages related to Makefiles: helm-make and make-it-so. The latter one is actually very interesting and deserves its own post, I should maybe just clean it up a bit.

Outro

I'll just cite Gall's law here:

A complex system that works is invariably found to have evolved from a simple system that worked. A complex system designed from scratch never works and cannot be patched up to make it work. You have to start over with a working simple system.

I think it applies from both sides w.r.t. my Emacs config: it kind of works, but I really wish it was reproducible from the start, before making it complex.