Upgrading to Emacs 28.0 for native compilation

This is a record of how I built Emacs 28 with native compilation on macOS (Intel), and the issues I encountered upgrading my config from 26.3. I'm primarily writing this for myself in case I run into similar problems in the future, but hopefully it can be useful for somebody else too.

1. Why I wanted to upgrade

Emacs 27.1 - faster JSON with libjansson

Emacs 27.1 was released in August 2020. One of the changes was that it introduced was support for libjansson, a C library for working with JSON, which is significantly faster than json.el. A place where this is particularly beneficial is for LSP performance, as the LSP clients and servers communicate using JSON.

Emacs 28 - faster everything with libgccjit

This was my motivation for the upgrade. 2 weeks ago the native compilation branch (led by Andrea Corallo and previously named gccemacs) was merged into master (the development branch for what will later become Emacs 28.1). This project uses libgccjit to perform ahead-of-time compilation of emacs-lisp bytecode (.elc files) to native code (new .eln files), which adds general latency improvements to Emacs across the board.

2. Install steps for MacOS

I used jimeh's build-emacs-for-macos script, which does most of the work for you. I ran:

  1. brew bundle - this installs all the dependencies contained in the Brewfile.
  2. ./build-emacs-for-macos --no-frame-refocus --git-sha 83a915d3dfafd5f3d737afe1e13b75e4dd3aef96 master - this compiles Emacs.
  3. cd builds && tar -xjf Emacs.app-\[master\]\[2021-04-25\]\[83a915d\]\[macOS-10.15\]\[x86_64\].tbz - this extracts the compiled Emacs.app from the builds directory.
  4. ./Emacs.app/Contents/MacOS/Emacs --debug-init - start Emacs and see what issues occur.
  5. After fixing a lot of new errors I replaced /Applications/Emacs.app with the new Emacs.app.

Some notes on the flags for the build script:

3. Enabling native compilation

Native compilation should be enabled by default. Some site elisp files will have been compiled during Emacs' compilation, but other libraries will be compiled asynchronously when you load them (which means you don't get all the benefits straight away - after starting Emacs you have to wait for libraries to be compiled).

It was immediately clear to me that this compilation was executing, as my *Warnings* buffer started to fill with warnings. Some were harmless, like "assignment to free variable" when compiling my init.el, and others were actual errors.

I only had to make one change to enable something related to native compilation. I had a couple of places where I was using (load-file) rather than (require), and these didn't seem to be compiled automatically, so I did:

(when (fboundp 'native-compile-async)
  (native-compile-async "path-to-my-file.el"))

4. Things that went wrong

I ended up having to fix a lot of small issues before I could run my existing init.el without it failing on startup (or on native compilation). Most of the problems were due to upgrading Emacs and individual package versions rather than native compilation itself.

jka-compr recursive load issue when opening Emacs.app

For some reason this did not affect Emacs when Emacs.app/Contents/MacOS/Emacs was executed from the terminal - only when opened as an application. Whenever a package had a dependency on jka-compr, it would hit a recursive load error:

Error (use-package): evil/:catch: Recursive load: "/Applications/Emacs.app/Contents/Resources/lisp/jka-compr.el.gz", "/Applications/Emacs.app/Contents/Resources/lisp/jka-compr.el.gz", "/Applications/Emacs.app/Contents/Resources/lisp/jka-compr.el.gz", "/Applications/Emacs.app/Contents/Resources/lisp/jka-compr.el.gz", "/Applications/Emacs.app/Contents/Resources/lisp/jka-compr.el.gz", "/Applications/Emacs.app/Contents/Resources/lisp/rect.el.gz", "/Users/mattduck/.emacs.d/eln-cache/28.0.50-6e08c520/evil-common-4cbe422e-ef770841.eln", "/Users/mattduck/.emacs.d/elpa/evil-20210424.1855/evil.elc"

There turned out be various issues reported for this (eg. here). It's caused by load-prefer-newer, which is a variable that controls what happens if Emacs finds multiple versions of the same file (.el, elc, .so). When true, Emacs will load the newest one.

The workaround was to disable load-prefer-newer before loading jka-compr:

(setq load-prefer-newer nil)
(require 'jka-compr)
(setq load-prefer-newer t)

This worked fine, but I'm not sure what the root cause is, or why I can find examples of this error going back a few years but I've never seen it before now.

package-refresh-packages hangs when using Marmalade

I was having issues with package-refresh-packages hanging, but they disappeared when I removed Marmalade from package-archives. I'm not sure if this was related to the upgrade or not. Either way I've hardly ever used Marmalade so I just removed it from my package-archives definition.

wrong number of arguments window–display-buffer 5

The signature of the builtin function window--display-buffer changed in 27.1 - it removed the fifth argument DEDICATED. For me this broke my fork of shackle, which uses this function in a couple of places. It's fixed in the upstream repo at https://depp.brause.cc/shackle/, so I just pulled in the fix.

pyvenv-tracking-mode slowing everything down

This was a weird one. I set pyvenv-workon globally in my init.el, but I also had had a dir-locals setting for a project that was setting pyvenv-workon to a project-specific virtualenv using a symbol like this:

((python-mode
  (pyvenv-workon . foo\.bar)))

After upgrading, when pyvenv-tracking-mode was enabled, python buffers were extremely slow to respond to input. I pinned the issue down to pyvenv and upgraded pyvenv to 20201227.1623, which didn't help. I eventually realised that pyvenv was constantly switching between my dir-locals virtualenv and my global virtualenv.

The reason for the constant virtualenv switching was that pyvenv-tracking-mode runs a function called pyvenv-track-virtualenv on post-command-hook, and this command was comparing pyvenv-virtual-env-name as a string ("foobar") to the dir-locals declaration for pyvenv-workon as a symbol (foo\.bar). Because the string wasn't equal to the symbol, the virtualenv would keep getting reset every time a command was run in the buffer.

Updating the dir-locals declaration to use a string fixed it. I'm not sure what changed to make this issue start occurring now, as it wasn't anything specifically in my setup or pyvenv.

An org-eldoc "wrong-number-of-arguments" error

This error appeared on init:

eldoc error: (wrong-number-of-arguments (lambda nil Return breadcrumbs when on a headline, args for src block header-line,
  calls other documentation functions depending on lang when inside src body. (or (org-eldoc-get-breadcrumb) (org-eldoc-get-src-header) (let ((lang (org-eldoc-get-src-lang))) (cond ((or (string= lang emacs-lisp) (string= lang elisp)) (if (fboundp 'elisp-eldoc-documentation-function) (elisp-eldoc-documentation-function) (let (eldoc-documentation-function) (eldoc-print-current-symbol-info)))) ((or (string= lang c) (string= lang C)) (if (require 'c-eldoc nil t) (progn (c-eldoc-print-current-symbol-info)))) ((string= lang css) (if (require 'css-eldoc nil t) (progn (css-eldoc-function)))) ((string= lang php) (if (require 'php-eldoc nil t) (progn (php-eldoc-function)))) ((or (string= lang go) (string= lang golang)) (if (require 'go-eldoc nil t) (progn (go-eldoc--documentation-function)))) (t (let ((doc-fun (org-eldoc-get-mode-local-documentation-function lang))) (if (functionp doc-fun) (progn (funcall doc-fun))))))))) 1)

It was coming from org-eldoc, which is part of org-plus-contrib. It went away when I upgraded from org-plus-contrib version 20200518 to 20210426 (in the org package repo).

lua-mode/:catch: Unknown rx form ‘symbol’

I ran into this error in lua-mode:

lua-mode/:catch: Unknown rx form ‘symbol’ Disable showing Disable logging

It can be fixed by removing the existing lua-mode.elc file.

Error: Wrong number of arguments (3 . 4)

This "wrong number of arguments" error appeared when loading various packages. I didn't even look into where this was occurring as it disappeared as soon as I upgraded the packages. The upgrades were:

Package Old version New version
projectile 20200329.1908 20210407.707
dockerfile-mode 20200106.2126 20210404.2224
lsp-mode 20200425.434 20210501.508
evil 20200417.1238 20210424.1855

LSP dependencies needed upgrading

After upgrading LSP, my Python setup stopped working (eg. the language server would return errors that it couldn't find various modules for my python projects). This was fixed by upgrading to the latest versions of python-language-server, pyls-black, pyls-mypy and plys-isort.

powerline.el: Error: List contains a loop ("22", . #0)

During native compilation, this warning is displayed for powerline:

Warning (comp): /Users/mattduck/.emacs.d/elpa/powerline-20200105.2053/powerline.el: Error: List contains a loop ("22", . #0)

This is an outstanding issue for powerline. For now the fix is to not compile it by doing (setq comp-deferred-compilation-deny-list '("powerline")).

use-package's :pin feature doesn't work

This was another strange one. In my init.el I was using the use-package :pin argument to pin org-mode to use the org package archive instead of melpa. I had got to the point where everything would work OK when loading Emacs the first time. But after native-compiling init.el, the next time I opened Emacs it would skip my use-package declaration for org-mode. It turned out that this only happened when I included the :pin argument.

Removing :pin fixed the issue, and didn't have any detrimental effect for me because I don't have multiple org versions installed anymore. I'm curious what this use-package issue is though and whether it's specific to my setup.

Loading an external elisp file failed if compiled

I was using :load-path with use-package to load some elisp related to managing windows and buffers, which I keep in a separate repo. The first time I opened Emacs this worked fine, but if I opened Emacs after native-compiling, some of my config would error because it referenced void symbols from this external file.

I'm not sure what this problem was - I just copied the contents of the external file inline to init.el to workaround it. This was acceptable for me as I had been planning to move that code inline anyway.

SVG support didn't work

I tried to call build-emacs-for-macos with the --rsvg flag, which is supposed to provide svg support via librsvg. The log output suggested that it was being compiled as expected, but when I opened Emacs svg wasn't supported. I didn't fix this, and haven't looked into what it is yet.

5. Too many errors?

I think this is an unacceptable amount of work for the majority of people. There were a lot of separate issues that I had to investigate, and most of them remain a mystery to me - it just wasn't practical to invest time in understanding all the causes. Fortunately there were easy workarounds and I didn't have to disable anything that I actually use.

Some of this is on me: I have a lot of config code which has been hacked together over 7+ years, tons of packages installed, I hadn't upgraded for a while, I used master rather than a stable release, etc. But I think some of it is just the nature of Emacs and the ecosystem - packages are supported by a small number of contributors, breaking changes are reasonably common, you won't always find issues reported when you encounter a problem.

I'd expect this to be slightly easier if you're using a distribution like Spacemacs or Doom, because of their popularity and because a large chunk of the configuration is managed for you. But if you're starting with vanilla Emacs and you want to customise it with a lot of packages, the reality is that you're going to have spend time debugging problems like this, and you probably have to be invested in DIY editors, Emacs, Lisp etc. for it to be worth it.

For me upgrading has been a nice improvement. Emacs feels noticeably quicker than I've experienced before, and more aligned with what you'd expect in a modern IDE. It will be pretty cool when native compilation gets a proper release in 28.1.

You can find my config on github, and you can watch Andrea talk more about native compilation here.

2021-May-05