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 except init.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.