Introduction
An Emacs package like dimmer.el
is unique in that it inhabits the
application all the time. When users report issues, they’re usually
reporting an unexpected interaction between dimmer.el
and one of the
other packages they happen to use.
My challenge is how to reproduce issues reported by users of packages that I don’t routinely use in my daily workflow. So here I’m going to describe a technique I use to create a simple standalone Emacs configuration.
What do you mean by “standalone”?
Emacs prefers to keep your configuration in a predefined path like
~/.emacs.d/
. However, what we are going to do is arrange things so
that Emacs reads its configuration from an alternate path, and any
packages you download or data files Emacs wants to save will also go
to this alternate path. In this article I’ll call this technique a
“standalone” Emacs configuration for reproducing bugs.
With this setup I can narrow down the smallest list of packages that will reliably get the bug to occur. Doesn’t matter if I use the packages or not, because this technique doesn’t conflict or interact with my personal configuration at all.
So, with that said, here’s how we do that.
Making a standalone init.el configuration
First, we need to override user-emacs-directory
. By default it will
probably point to ~/.emacs.d/
. However, in our standalone setup
we’re going to set the path like so:
(setq user-init-file (or load-file-name (buffer-file-name)))
(setq user-emacs-directory (file-name-directory user-init-file))
From this point forward any temporary file written by Emacs will go
into the same directory where init.el
lives. It’s almost as if
that directory were ~/.emacs.d/
.
Next, we need to initialize the package manager and configure any additional package archives you need to download packages. In this example we need only the MELPA archive:
(require 'package)
(add-to-list 'package-archives '("melpa" . "https://melpa.org/packages/") t)
(setq package-enable-at-startup nil)
(package-initialize)
(when (not package-archive-contents)
(package-refresh-contents))
Next, we need to install some packages using the standard Emacs
package manager. While it would be fine in this example to call
package-install
directly, I’m showing you a more elaborate way.
This little snippet of code lets you optionally “pin” different
packages to different repositories by supplying a list of pairs,
package name and repo:
(setq package-pinned-packages
'((bind-key . "melpa")
(diminish . "melpa")
(use-package . "melpa")))
(dolist (p (mapcar 'car package-pinned-packages))
(unless (package-installed-p p)
(package-install p)))
I recently used this to “pin” a package to an alternate repository, because it was that alternate version of the package that caused the bug.
Everything up to this point was the a basic boilerplate I use every
time. Down below, I’ll work with use-package
to install and
configure all the packages I need to reproduce the bug I’m hunting.
So for this scenario let’s say I’m trying to debug an interaction
between ivy
, swiper
, ivy-posframe
and dimmer
. In that case,
here is a minimal configuration that installs just those packages:
(use-package ivy
:ensure t
:config (ivy-mode t))
(use-package swiper
:ensure t
:bind ("C-s" . swiper))
(use-package counsel
:ensure t
:config (counsel-mode t))
(use-package ivy-posframe
:ensure t
:config
(setq ivy-posframe-display-functions-alist
'((swiper . ivy-posframe-display-at-point)
(complete-symbol . ivy-posframe-display-at-point)
(counsel-M-x . ivy-posframe-display-at-window-bottom-left)
(t . ivy-posframe-display)))
(ivy-posframe-mode t))
(use-package dimmer
:ensure t
:config
(setq dimmer-fraction 0.4)
(setq dimmer-debug-messages 3)
(dimmer-mode t))
Got it? Ok, let’s stop and review.
Review
We have described an Emacs lisp init.el
file which overrides the
user-emacs-directory
to point to a different directory, and inside
contains just enough configuration to load a few packages you are
debugging. This init.el
allows us to keep the test configuration
isolated from your real Emacs configuration files.
Usage
If I save my configuration file into a new, empty directory I can then launch Emacs and test this config like so:
$ mkdir test-case
$ cp init.el test-case
$ emacs -q -l test-case/init.el --batch
See where I launched Emacs in --batch
mode? This prevents Emacs from
opening an interactive window, but instead, it loads the configuration
file and installs all the packages I listed in my config. It’s
useful to watch the console output from this step to make sure
everything installs properly without serious error messages.
After downloading those packages Emacs simply exits. I can then look
at the contents of test-case/elpa
and confirm the packages I need
are all in there.
Now, finally, I can run Emacs without the --batch
to get an
interactive session:
$ emacs -q -l test-case/init.el
… and it’s time to debug.
Debugging workflow
Using this configuration makes some things really convenient in your debugging workflow:
-
Sharing with other developers
One
init.el
file is all you need to communicate your setup with others who might be helping you. And because this isn’t your real Emacs configuration there’s no worry if you have things in your configuration you’d rather not share. -
Hacking on the packages themselves
Because you have copies of packages in
test-case/elpa
you can edit those files and insert debugging code or change the implementations in order to track down the bug. -
Resetting
At any point, if you think you’ve messed up one of those packages, you can delete everything in the
test-case
folder exceptinit.el
and you’ve reset yourself to a clean state. Run emacs again as described above, and all the needed dependencies will be downloaded into that test directory.
Wrapping up
Here is the complete listing of init.el
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; run with:
;; /usr/local/bin/emacs -q -l init.el
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; set user-emacs-directory to the directory where this file lives
;; do not read or write anything in $HOME/.emacs.d
(setq user-init-file (or load-file-name (buffer-file-name)))
(setq user-emacs-directory (file-name-directory user-init-file))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; set custom-file to write into a separate place
(setq custom-file (concat user-emacs-directory ".custom.el"))
(when (file-readable-p custom-file) (load custom-file))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; configure melpa (and other repos if needed)
(require 'package)
(add-to-list 'package-archives '("melpa" . "https://melpa.org/packages/") t)
(setq package-enable-at-startup nil)
(package-initialize)
(when (not package-archive-contents)
(package-refresh-contents))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; bootstrap `use-package'
(setq package-pinned-packages
'((bind-key . "melpa")
(diminish . "melpa")
(use-package . "melpa")))
(dolist (p (mapcar 'car package-pinned-packages))
(unless (package-installed-p p)
(package-install p)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; install and configure packages
(use-package ivy
:ensure t
:config (ivy-mode t))
(use-package swiper
:ensure t
:bind ("C-s" . swiper))
(use-package counsel
:ensure t
:config (counsel-mode t))
(use-package ivy-posframe
:ensure t
:config
(setq ivy-posframe-display-functions-alist
'((swiper . ivy-posframe-display-at-point)
(complete-symbol . ivy-posframe-display-at-point)
(counsel-M-x . ivy-posframe-display-at-window-bottom-left)
(t . ivy-posframe-display)))
(ivy-posframe-mode t))
(use-package dimmer
:ensure t
:config
(setq dimmer-fraction 0.4)
(setq dimmer-debug-messages 3)
(add-to-list 'dimmer-exclusion-regexp-list "^ \\*ivy-posframe-buffer\\*$")
(dimmer-mode t))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
And that’s it. I hope this was helpful. If you have questions or problems, go ahead and leave a comment below.