Skip to content

Notes on My Emacs Customization

James Craig Burley edited this page May 30, 2018 · 1 revision

What

My Emacs setup (for each machine) consists of a git repository cloned into ~/github/UnixHome/ with an .emacs.d/ directory that has an init.el file, a systems/ directory for per-machine customization and other files, and whatever else is suitable to put under source control.

Meanwhile, ~/.emacs.d/ has either a symlink, or explicit (load-file ...), from init.el to ~/github/UnixHome/.emacs.d/init.el, its own systems/ directory for scratch, cache, and other temporary files, and the usual stuff that Emacs and its packages put there (such as auto-save-list and elpa).

Why

As I'm currently using four or five "pet" systems regularly, involving three OSes (Windows 7, Mac OS X High Sierra, and Ubuntu 16.04 Linux), and am a longtime user of Emacs, I wanted my Emacs customizations to be under source control so all existing systems, plus any new ones (say, VMs running under Parallels on my MacBook Pro), could quickly share them, as well as improvements and fixes made to them, while:

  • Providing for local customization such as done via M-x customization
  • Offering quick startup times
  • Not requiring putting Emacs into client/server mode
  • Providing a "gentle", incremental path for improving my Emacs setup
  • Avoiding stale package-repository info without requiring too much hand-holding by me

The second item actually drove improving on the first, because I was experiencing regular startup delays in the range of 10-60 seconds for use as $EDITOR/$VISUAL in my environment.

The third item was driven mainly by not finding a clear "best-practice" solution for how and when to start up the server, distinguish it from other potential configurations (not a current need of mine, I admit), and then gracefully "retire" it after non-use. This was an issue mainly for the remote Ubuntu VPC that serves my personal email and web needs, as it is (or at least was, until recently) a tad memory-starved, such that having even an Emacs server sitting around unused seemed less than ideal.

The fourth item arose during research into various recommended Emacs setups, including those making use of "org-mode", all of which seemed more complicated and having steeper learning curves than I was willing to tackle, in combination with not necessarily meeting my own needs (as described above).

How

Put init.el Under Source Control (git)

init.el starts off by defining two variables denoting where system-specific, non-source-control and source-control, files will live, and ensuring those directories exist (though no attempt is made to actually register the latter with the source-control system in use). The second of these variables is defined thus:

;; Put customizations in per-hostname files.
(defvar system-specific-init-dir
 (concat "~/github/UnixHome/.emacs.d/systems/" (downcase (system-name)))
 "System-specific customization and other files are stored here.")
(make-directory system-specific-init-dir t)
(setq custom-file (concat system-specific-init-dir "/customizations.el"))

Local customizations are redirected from init.el to customizations.el in the source-controlled directory:

;; Put customizations in per-hostname files.
(setq custom-file (concat system-specific-init-dir "/customizations.el"))

(The other, non-source-controlled (but system-specific), directory will be used for improving startup times.)

Finally, the last thing init.el does is load local customizations, if any:

(load custom-file t)  ; No error if file doesn't exist.

Again, it's entirely up to the user to use git commit, git add, git push, or whatever is appropriate to push local changes to ${UNIXHOME}/.emacs.d/ up to the git server(s) -- and git pull them back down on other machines. Having init.el take care of this seemed beyond its scope, though I'm not sure what would be the best approach to automating this (cron jobs?).

There's currently no facility for loading other source-controlled, "fixed", file(s) on a per-system basis. I'll add one later if I want per-system customization outside the M-x customize framework and file.

Avoid Refreshing Packages

Experimentation showed that (package-refresh-contents), especially after the number of (add-to-list 'package-archives ...) invocations that preceded it, was the primary consumer of real time during startup, given my configuration.

Putting package refresh in the background is possible, but I wasn't sure of the implications for subsequent package-management actions run during init.el, nor for quickly invoking Emacs on a package-using file (such as core.clj). Further, I didn't want unnecessary CPU, memory, and network traffic consumed each time I started up Emacs, even if such consumption wasn't readily visible during normal use.

I'm especially concerned about all those Emacs instances starting up, around the world, and immediately hitting those repositories. Could that be why they seem to be slow or even non-responsive at times?

And figuring out whether, nevermind how, to do the refresh "on demand" seemed like a major undertaking for a newcomer to the Emacs package system like myself. (I'm not sure it's even possible without losing critical functionality.)

So the approach I took herein was to refresh packages only the first time Emacs is invoked per day (on a given system).

I implemented this using a "package-refresh stamp" file that contains the date (in YYYY-MM-DD format) on the first line, then the stringized form of the contents of package-archives, written whenever init.el refreshes the packages, but first checked whether it already contains what would be written (indicating that a previous Emacs instance likely refreshed the same set of packages).

(This approach fails to re-try package refreshes when one or more of them experiences an error. So, a temporary connection problem would mean that particular repository is not locally updated until at least the next day.)

As mentioned above, a variable is defined (near the top of init.el) that specifies where per-system scratch, cache, and other temporary files belong:

(defvar system-specific-scratch-dir
 (concat "~/.emacs.d/systems/" (downcase (system-name)))
 "System-specific scratch, cache, and other temporary files are stored here.")
(make-directory system-specific-scratch-dir t)

The implementation starts off setting the contents of the "stamp" itself, based on the value of the package-archives var at that point:

(defvar package-stamp
      (concat
       (format-time-string "%Y-%m-%d\n")
       (prin1-to-string package-archives))
      "String containing today's date on first line followed by stringized var package-archives.")

Then, the file in which the stamp is maintained is defined:

(defvar package-stamp-file
  (concat system-specific-scratch-dir "/package-refresh.STAMP")
  "Name of file in which package stamp is written -- local to each machine.")

Two helper functions, which IMO really should just be in Emacs anyway (as it's rather hard to discover this functionality using M-x apropos), follow:

(defun read-file-contents (f)
  "Read contents of a file and return the result as a string."
  (with-temp-buffer
    (insert-file-contents f)
    (buffer-string)))

(defun write-file-contents (s f)
  "Write given string to a specified file."
  (write-region s nil f))

A function that determines whether the "stamp" file exists and has the same contents as the current stamp follows:

(defun file-contents-= (s f)
  "Return t if file exists and contains exactly the string, nil otherwise."
  (and (file-exists-p f)
       (string= s (read-file-contents f))))

Finally, the packages are refreshed if they haven't already been in this instance and haven't been already today by an earlier instance:

;; Download the ELPA archive description if needed.
;; This informs Emacs about the latest versions of all packages, and
;; makes them available for download.
(unless (or package-archive-contents
            (file-contents-= package-stamp package-stamp-file))
  (package-refresh-contents)
  (write-file-contents package-stamp package-stamp-file))

This seems (so far) to strike a reasonable balance between having up-to-date package information and keeping startup time short, yet reliable.

Clone this wiki locally