Rationale

It turns out that the Emacs package management system, package.el, doesn’t perform SSL certificate verification without some fairly involved wrangling. My Emacs configuration is something that I want to be able to clone and run on systems where it might be a real pain to perform the wrangling needed to ensure packages may be downloaded securely over encrypted HTTP.

Another issue with downloading packages from MELPA, the most popular repository for package.el, is that some packages are pulled into that repository from the EmacsWiki over unencrypted HTTP.

A further problem with MELPA is that it moves very fast, and new versions of packages that are not compatible with each other or perhaps your configuration means that you can find yourself with a broken editor in the middle of trying to get work done. To deal with this issue there is MELPA Stable, which contains hopefully-stable releases of packages that are more likely to be compatible with other packages. The problem is that many packages are in MELPA but not MELPA Stable because the author has not tagged any releases, and of the packages that are in MELPA Stable, many require newer versions of their dependencies than the versions of those dependencies available in MELPA Stable.

In short, package.el and MELPA are not dpkg, apt and the Debian Stable archive. Hopefully someday they will be. But for the moment, I don’t want to manage my Emacs packages this way.

Managing packages as git subtrees

An alternative is to manage package repositories as git subtrees. Assuming that your ~/.emacs.d/ is kept in a git repository, we can run

$ cd ~/.emacs.d
$ git subtree add --squash -P pkg/magit https://github.com/magit/magit 2.3.0

and then Magit becomes available in ~/.emacs.d/pkg/magit. The following lisp will add all the dirs ~/.emacs.d/pkg/* and ~/.emacs.d/pkg/*/lisp to your load-path; you can modify this by changing the variable globs:

;;;; ---- package management ----

;; be sure not to load stale bytecode-compiled lisp
(setq load-prefer-newer t)

;; this is where all subtree packages are
(defconst emacs-pkg-dir (concat user-emacs-directory "pkg"))

;; load up f, and its dependencies s and dash, so we can use `f-glob'
;; and `f-join'
(dolist (pkg '("f.el" "dash.el" "s.el"))
  (add-to-list 'load-path (concat emacs-pkg-dir "/" pkg)))
(require 'f) (require 's) (require 'dash)

;; helper function
(defun expand-all-globs (root globs)
  (let ((do-glob (lambda (glob) (f-glob (f-join root glob)))))
    (apply 'nconc (mapcar do-glob globs))))

;; now add all my pkg lisp directories
(let* ((globs '("*" "*/lisp"))
       (dirs (expand-all-globs emacs-pkg-dir globs)))
  (dolist (dir dirs)
    (when (file-directory-p dir)
      (add-to-list 'load-path dir))))

;; finally put my own site-lisp at the front of `load-path'
(add-to-list 'load-path (concat user-emacs-directory "site-lisp"))

;; we will use use-package to load everything else
(require 'use-package)

When you want to update to a new version of a package,

$ cd ~/.emacs.d
$ git subtree pull --squash -P pkg/magit https://github.com/magit/magit 2.3.1

Note:

  • This commits the source code of Magit to your ~/.emacs.d/ git repository. So when you clone your config to a new machine, all your packages will already be there and Emacs won’t have to download them (potentially insecurely).
  • There’s no dependency management. You’ll have to add subtrees for every dependency. At present, if you don’t update your packages often, this is not too onerous.
  • You should run C-u 0 M-x byte-recompile-directory ~/.emacs.d/pkg RET periodically (normally, package.el would do this for you).

Shell script

Here is a shell script to reduce typing in adding and updating subtrees. It also logs git repository clone URIs and versions fetched to a file ~/.emacs.d/pkg/subtrees so that you can find the URI to use when you want to do an update:

$ cat ~/.emacs.d/pkg/subtrees
https://github.com/magit/magit 2.3.1
https://github.com/lewang/flx v0.6.1

Use it like this:

$ emacs-pkg-subtree add https://github.com/magit/magit 2.3.0
$ emacs-pkg-subtree pull https://github.com/magit/magit 2.3.1
#!/bin/bash

# emacs-pkg-subtree --- manage Emacs packages as git subtrees in your dotfiles git repo

# Author/maintainer    : Sean Whitton <spwhitton //ANTI-SPAM \\ spwhitton.name>
# Instructions for use : https://spwhitton.name/blog/entry/emacs-pkg-subtree/

# Copyright (C) 2015  Sean Whitton.  Released under the GNU GPL 3.

DEST="$HOME/.emacs.d/pkg"

set -e

if [ "$3" = "" ]; then
    echo "$(basename $0): usage: $(basename $0) add|pull git_clone_uri ref" >&2
    exit 1
fi

cd $DEST

op="$1"
uri="$2"
repo="$(basename $2)"
pkg="${repo%%\.git}"
ref="$3"
top="$(git rev-parse --show-toplevel)"
prefix="${DEST##$top/}/$pkg"

cd $top
clean="$(git status --porcelain)"
if [ ! -z "$clean" ]; then
    echo "commit first" >&2
    exit 1
fi

if [ "$op" = "add" ]; then
    if [ ! -e "$DEST/$pkg" ]; then
        git subtree add --squash --prefix $prefix $uri $ref
        echo "$uri $ref" >> $DEST/subtrees
        git add $DEST/subtrees
        git commit -m "updated Emacs packages record"
    else
        echo "you already have a subtree by that name" >&2
        exit 1
    fi
elif [ "$op" = "pull" ]; then
    git subtree pull --squash --prefix $prefix $uri $ref
    sed -i -e "s|^${uri} .*$|${uri} ${ref}|" $DEST/subtrees
    git add $DEST/subtrees
    git commit -m "updated Emacs packages record"
else
    echo "$(basename $0): usage: $(basename $0) add|pull git_clone_uri ref" >&2
    exit 1
fi