Friday, December 07, 2012

ELPA and initialization

ELPA is one of the biggest improvements to Emacs 24. It stands for the Emacs Lisp Package Archive, and it give Emacs users a way to install and maintain packages from centralized repositories. ELPA takes care of browsing the list of packages, finding out more about each one, downloading each package, downloading dependent packages, byte-compiling each package, and installing them in a standard place.

Using ELPA already will simplify your config file. You no longer have to change the load-path for each package. All ELPA packages will be on the load-path automatically.

Furthermore, some packages are specifically designed to give you a sane startup environment, notably the starter-kit package. Just downloading the package from ELPA will get you its extra initialization.

However, it'd be best to not have to do any manual downloading for core packages that make up the customization we want. It'd be nice if we can set up our ~/.emacs.d/ directory on a new machine from git, and just start emacs and have everything set up for us.

Right now, in my initialization file, I have the following code that will set up packages on a new machine:

(require 'package)
(setq package-archives '(("ELPA" . "")
                         ("gnu" . "")
                         ("melpa" . "")
                         ("marmalade" . "")
                         ("technomancy" . "")))

;; Package setup, taken from
(setq ash-packages

;;; install missing packages
(let ((not-installed (remove-if 'package-installed-p ash-packages)))
  (if not-installed
      (if (y-or-n-p (format "there are %d packages to be installed. install them? "
                            (length not-installed)))
          (progn (package-refresh-contents)
                 (dolist (package not-installed)
                   (package-install package))))))

First, this code loads the package code with a require statement, then sets the list of ELPA archives I use. Then, it sets up a list of packages I'd like to install in the variable ash-packages. After a necessary initialization, it looks at each of the packages, and for each one that isn't already installed, it installs it from ELPA. As the comment mentions, I took this whole thing from zane's dotemacs repository. As I mentioned in an earlier post, stealing other people's customizations is a time-honored emacs tradition.

Further down in the file, you will see that for some of these packages, some basic configuration happens. I try to group all configuration for a package together. It would be nice if we could specify the package for download and the configuration together, since then we could remove an unwanted package and all configuration at once. I don't think this is worth it, though, at least right now. Maybe I'll pursue this in a further blog post.

Finally, it's worth noting that I do all my customization in eval-after-load functions. The purpose of those functions is that the contents of them are loaded only when the package is required or autoloaded, which avoids issues with missing functions or variables. If the package is already loaded, then everything is evaluated immediately. It's also important note that these are written with the function to execute quoted (since we don't want to execute it right away).

For example, here's something that I use to setup the key-chord package:

(eval-after-load 'key-chord
     (key-chord-mode 1)
     (key-chord-define-global "jk" 'dabbrev-expand)
     (key-chord-define-global "l;" 'magit-status)
     (key-chord-define-global "`1" 'yas/expand)
     (key-chord-define-global "-=" (lambda () (interactive) (switch-to-buffer "*compilation*")))

     (key-chord-define-global "xb" 'recentf-ido-find-file)
     (key-chord-define-global "xg" 'smex)
     (key-chord-define-global "XG" 'smex-major-mode-commands)
     (key-chord-define-global "fj" 'ash-clear)))

When a file that provides key-chord is loaded, this will execute. If key-chord is already loaded when this eval-after-load is evaluated, it will execute immediately.

Sometimes you want to have only load things when two packages are simultaneously loaded. Then you can nest one eval-after-load in another. I do that in that following elisp:

(eval-after-load 'multiple-cursors
     (global-set-key (kbd "C-c m m") 'mc/edit-lines)
     (global-set-key (kbd "C-c m a") 'mc/edit-beginnings-of-lines)
     (global-set-key (kbd "C-c m e") 'mc/edit-ends-of-lines)
     (global-set-key (kbd "C-c m r") 'mc/set-rectangular-region-anchor)
     (global-set-key (kbd "C-c m =") 'mc/mark-all-like-this)
     (global-set-key (kbd "C-c m n") 'mc/mark-next-like-this)
     (global-set-key (kbd "C-c m p") 'mc/mark-previous-like-this)
     (global-set-key (kbd "C-c m x") 'mc/mark-more-like-this-extended)
     (global-set-key (kbd "C-c m u") 'mc/mark-all-in-region)
     (eval-after-load 'key-chord
          (key-chord-define-global "zm" 'mc/edit-lines)
          (key-chord-define-global "za" 'mc/edit-lines)
          (key-chord-define-global "ze" 'mc/edit-lines)
          (key-chord-define-global "zr" 'set-rectangular-region-anchor)
          (key-chord-define-global "z=" 'mc/mark-all-like-this)
          (key-chord-define-global "i\\" 'mc/mark-all-like-this)
          (key-chord-define-global "zn" 'mc/mark-next-like-this)
          (key-chord-define-global "zp" 'mc/mark-previous-like-this)
          (key-chord-define-global "zx" 'mc/mark-more-like-this-extended)
          (key-chord-define-global "zu" 'mc/mark-all-in-region)))))

This is important! If we were to to put all the key-chord-define-global in the top-level eval-after-load, then if key-chord were not downloaded from ELPA, or not loaded for some reason, this would break, throwing an error that key-chord-define-global is undefined. Doing things this way is much safer.

However, you can't do this all the time. If we were to take this statement:

(eval-after-load 'key-chord
     (key-chord-mode 1)
     (key-chord-define-global "l;" 'magit-status)))
and turn it into this:
(eval-after-load 'key-chord
     (key-chord-mode 1)
     (eval-after-load 'magit
       '(key-chord-define-global "l;" 'magit-status))))

That would pose an issue. It would protect us against the case where the magit package isn't installed, and therefore we shouldn't have a keychord defined. However, if it was installed, we wouldn't have a keybinding, and unless we run (require 'magit), this keybinding wouldn't appear. We don't really want to run that, though, because an autoload is more efficient, and magit-status is an autoload. That means that the magit package may be unloaded, but we still have the entry functions from the package. When the user invokes any of those commands, then magit is loaded at that point.

Autoloads make our initialization files load much quicker. I think it's better to use them then to require everything, so I don't have nesting eval-after-load statements for anything autoloaded.

Here's the lessons we can take from this:
  1. Set up your ELPA such that you have a good set of package-archives.
  2. Set things up so that you have a list of must-have ELPA packages, which are downloaded when they don't exist.
  3. Customize through eval-after-load, and keep all your customization for a package in one place.
  4. When you are customizing things that only exist when two packages are loaded at the same time, use nested eval-after-loads.
  5. The exception to the above rule is autoloads. There is no need to nest an eval-after-load for an autoloaded function. You can usually just assume that any major function in a package is autoloaded, but if you want to check, jump to the source and look for a comment above that says ;;;###autoload.


rawshark said...

is there a way to customize where the elpa generated files (for example, foobar-autoloads.el) go? I tried putting my Emacs setup in github, and the generated autoloads files keep causing git conflicts (I generate them on machine 1, generate them on machine 2, and some timestamps are making git complain

Andrew Hyatt said...

I wouldn't check in your "elpa" directory in your github repository. There's no point with a setup like I explain here: each new computer will retrieve all the ELPA repositories you want installed.

Tom Slee said...

You say "stealing other people's customizations is a time-honored emacs tradition".

I'm carrying it on and stealing yours. Thanks for posting.

Steve Purcell said...

I'm not a big fan of keeping a single list of desired packages: I personally prefer to have a require-package function which I can place next to the code which configures that package, as seen here.