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:
brew bundle
- this installs all the dependencies contained in the Brewfile../build-emacs-for-macos --no-frame-refocus --git-sha 83a915d3dfafd5f3d737afe1e13b75e4dd3aef96 master
- this compiles Emacs.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../Emacs.app/Contents/MacOS/Emacs --debug-init
- start Emacs and see what issues occur.- After fixing a lot of new errors I replaced
/Applications/Emacs.app
with the newEmacs.app
.
Some notes on the flags for the build script:
- I chose commit
83a915d3dfafd5f3d737afe1e13b75e4dd3aef96
because it was the most recent commit in jimeh's list of known good commits for native-comp. - Native compilation is controlled by the
--with-native-comp
flag, but is enabled by default. --no-frame-refocus
prevents Emacs from raising another frame when a frame is closed. See Álvaro Ramírez's post on this.
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.