Compiling Emacs 29 for Debian/Ubuntu/Mint

I wanted to explore some of the new features in Emacs 29 (in particular Eglot). The version of Emacs that ships with my version of Linux Mint is Emacs 27, so I decided to build Emacs 29 from scratch.

This turned out to be absolutely painless; but installing the new version seamlessly into the existing environment did cause some hang-ups.

Compilation

The source tarball can be downloaded from one of the mirrors. To handle the dependencies, I used a build container with bind mounds, as described in a previous post.

The commands to run inside the container:

# Enable source repos
sed -i 's/# deb-src/deb-src/' /etc/apt/sources.list

apt update

apt build-dep emacs

apt install libgnutls28-dev libgtk-3-dev libwebkit2gtk-4.1-dev \
	gnutls-bin gnutls-dev libgccjit0 libgccjit-10-dev \
	libjansson4 libjansson-dev gnutls-bin libtree-sitter-dev \
	gcc-10 imagemagick libmagick++-dev libwebp-dev webp libxft-dev libxft2
	
export CC=/usr/bin/gcc-10  && export CXX=/usr/bin/gcc-10 && ./autogen.sh

./configure --prefix=/opt/emacs29

make
make install

Don’t ask me about the list of dependencies — it is based on this gist, but changes the dependency on gnutls to the separate packages gnutls-bin and gnutls-dev as required for my distro.

The --prefix=/opt flag selects /opt as the installation directory, instead of the default of /usr/local. The installation will create a directory emacs29 underneath /opt and place all its artifacts there. The container’s /opt directory was mapped, via a bind mount, to a directory on the host system.

Installation

I prefer to install third-party (non-distro) packages into /opt, rather than scattering them throughout /usr or /usr/local. In this case, I placed the directory emacs29, containing all build artifacts, into /opt on the host system. The main binary is at /opt/emacs29/emacs and starts up without any problems.

Not surprisingly, emacs29 does not find any previously installed packages: they are in their default location /usr/share/emacs/site-lisp. Unfortunately, adding this location to the load-path, using

(add-to-list 'load-path "/usr/share/emacs/site-lisp/")

does not really work.

Packages in site-lisp are installed by my distro, or through my distro’s package manager. They are usually autoloaded. For instance, the package for rainbow-mode is installed in site-lisp, and M-x rainbow- TAB will autocomplete just fine, without loading or requiring anything in my .emacs. I can also refer to 'rainbow-mode in my .emacs and the expression will be recognized. However, in emacs29, this does not work, not even after adding the directory to the load-path as described above: Emacs still does not know anything about rainbow-mode; I need to require the package explicitly in .emacs. (Adding to load-path in the early init file also does not work.)

Apparently, Emacs treats files underneath its default location(s) differently than arbitrary directories on its load-path. I guess this must be mentioned somewhere in the documentation, but I have not been able to find a reference to it. It is also not totally clear whether this is an Emacs feature, or due to some Debian packaging. (The Debian Emacs Policy makes for fascinating, although slightly intimidating, reading. Another interesting reference point is /usr/share/emacs/NN.N/lisp/startup.el, which contains the command Emacs executes when launched.)

By the way, I don’t think this problem has anything to do with descendant directories not being mentioned explicitly in the load-path: the site-lisp directory contains a subdirs.el file that should take care of that.

My solution is practical, but a little hacky: I created a symlink from the old site-lisp directory to the location of the new emacs29 installation:

cd /opt/emacs29/share/emacs
sudo rm -rf site-lisp
sudo ln -s /usr/share/emacs/site-lisp/ .

Now all the packages in the “old” site-lisp directory are detected and autoloaded when emacs29 starts up. (I don’t think this will re-enable the Debian start-up modifications, but so far I have not noticed any ill effects to them missing.)

Customization

Finally, I broke my customizations for each version of Emacs into separate files, and let the .emacs file load whichever one is appropriate:

(if (version< (number-to-string emacs-major-version) "29")
    (load-file (expand-file-name "~/.emacs.d/init27.el"))
  (load-file (expand-file-name "~/.emacs.d/init29.el")))

Of course it is also possible to retain a single .emacs file and use conditional expressions inside of it.