GNU guix is a functional package manager, that can be installed on any distribution and used to manage packages, including emacs packages.
How is this different from straight.el
straight.el is also a functional package manager, but unlike guix, it only manages emacs packages, while with guix you can include external programs that you will make use of from emacs (like ripgrep), emacs itself, fonts, and treesitter grammars.
When using straight, you would typically install emacs with your distributions package manager. Updating your system could upgrade your emacs, and the new version could have an incompatibility with one of the packages you use, or it could have a change in behavior that is disruptive to you.
By using a functional package manager to manage your whole emacs environment, including emacs itself, you can have complete stability, as well as consistency across different distros and computers.
Guix Basics
You’ll need to install guix on your system. See installation instructions here: https://guix.gnu.org/manual/en/html_node/Binary-Installation.html
I would not suggest installing GUI apps in your default profile which is loaded on startup, as there are reports of them conflicting with your DE and other apps installed with your package manager. I’ve never had any issues with using emacs in a profile.
There are several main components creating a reproducible environment with guix
- The guix profile where your packages will be located. We’re using
$HOME/.emacs.d/guix/package-profile
. - The guix profile where the guix you use to install the packages will
be located. We’re using
$HOME/.emacs.d/guix/pull-profile
- Your
channels-lock.scm
, generated withguix describe
and committed into the repository where your emacs config lives - Your
manifest.scm
file. It’s convenient to generate this with org tangle, but i commit it to make it easier to get up and running on a fresh system.
If you’ve ever used python’s virtualenv, you have a pretty good idea
of what a guix profile is like. You install packages into a directory,
and when you want to use those packages, you source an automatically
generated file that will edit your $PATH and other environmental
(e.g. $EMACSLOADPATH
for emacs) variables to make the packages
accessible.
When you install guix it will create a profile for you in
$HOME/.guix-profile
and create a file in /etc/profile.d
to
automatically load it. guix package
will act on this profile by
default. If you want to install a package from guix to be available
globally for your user, you can install them to this profile, but we
want to use a separate profile just for our emacs environment.
You can specify an alternate profile by exporting the $GUIX_PROFILE
environmental variable, or by using the -p
or --profile
flags with
the guix command
This command would install emacs and emacs-magit into the profile
$HOME/.emacs.d/guix/package-profile
. If the profile doesn’t exist
guix will automatically create it.
mkdir -p "$HOME/.emacs.d/guix"
guix package --profile="$HOME/.emacs.d/guix/package-profile" --install emacs emacs-magit
Example of loading the profile and launching emacs
# Load the profile
GUIX_PROFILE="$HOME/.emacs.d/guix/package-profile"
. "$GUIX_PROFILE/etc/profile"
emacs
That’s all we need to do to create and use our emacs profile. We can launch emacs right now and use magit, but this way of installing packages isn’t reproducible. The version of emacs and emacs-magit that gets installed will depend on what version of guix you use.
See also:
The version of packages guix will install is determined the version of guix you
use. guix pull
is used to manage guix versions.
By default, running guix pull
with no arguments will update guix to
the latest version. To install a specific version of guix, we can use
guix describe --format=channels
to generate a channels-lock.scm
file that describes
the current version of guix, then use guix pull
with the -c
or
--channels
flags to rebuild it. This is how we ensure that our environment
is reproducible. The same version of guix with the same manifest will always
install the same package versions, and by commiting channels-lock.scm
to our
dotfiles repo, we can always recreate the version of guix we used at that time.
Just like how guix packages are installed in profiles, so is guix
itself. By default, this is $HOME/.config/guix/current
. Just like
with guix package
, you can use -p
or --profile
with guix pull
to use a different profile. This means you can have multiple versions
of guix
that install different versions of packages on the same
machine.
To make our emacs environment reproducible, we’ll use two guix
profiles. Just like last time, we’ll install the packages into
$HOME/.emacs.d/guix/package-profile
, but this time, instead of
installing packages with the default guix, we’ll use guix in a profile
specifically for our emacs environment. I’ll use
$HOME/.emacs.d/guix/pull-profile
.
Install the latest version of guix into our profile
guix pull --profile="$HOME/.emacs.d/guix/pull-profile"
Generate channels-lock.scm
GUIX_PROFILE="$HOME/.emacs.d/guix/pull-profile"
. "$GUIX_PROFILE/etc/profile"
# Our guix should now be $HOME/.emacs.d/guix/pull-profile/bin/guix
which guix
# Generate our initial =channels-lock.scm= Commit this to your emacs dotfiles repository.
guix describe --format=channels > $HOME/emacs-dotfiles/channels-lock.scm
Now, when you want to rollback to a known working configuration or
install your emacs environment on another pc you can use this
channels-lock.scm
file.
guix pull --channels="$HOME/emacs-dotfiles/channels-lock.scm" --profile="$HOME/.emacs.d/guix/pull-profile"
When you want to upgrade your environment use guix pull without the
channels-lock.scm
file, then after verifying that everything is
working as you expect run guix describe
again and update your
channels-lock.scm
file.
Now that we have a reproducible guix, we can use it to install packages and be sure that we will get the same version every time.
GUIX_PROFILE="$HOME/.emacs.d/guix/pull-profile"
. "$GUIX_PROFILE/etc/profile"
mkdir -p "$HOME/.emacs.d/guix"
guix package --profile="$HOME/.emacs.d/guix/package-profile" --install emacs emacs-magit
Launching emacs the same as before, with
GUIX_PROFILE="$HOME/.emacs.d/guix/package-profile"
. "$GUIX_PROFILE/etc/profile"
emacs
When running guix pull
commands, it doesn’t really matter if the
guix you’re using is $HOME/.config/guix/current/bin/guix
or
$HOME/.emacs.d/guix/pull-profile/bin/guix
, but when installing
packages or using guix describe make sure that you are using
$HOME/.emacs.d/guix/pull-profile/bin/guix
.
See also:
- https://guix.gnu.org/manual/en/html_node/Invoking-guix-pull.html
- https://guix.gnu.org/blog/2018/multi-dimensional-transactions-and-rollbacks-oh-my/
Instead of listing all of the packages we want in our emacs
environment in the guix package
command, we can instead use the -m
or --manifest
options to pass a manifest.scm
, which is at it’s
simplest essentially a list of package names.
(specifications->manifest
'("emacs"
"emacs-magit"))
Explaining how to write guix package definitions is outside the scope of this post. See: https://guix.gnu.org/cookbook/en/html_node/Packaging-Tutorial.html
But here is an example of how to define and use a custom package in your manifest alongside packages from the guix repos.
(use-modules (guix packages)
((guix licenses) #:prefix license:)
(guix build-system emacs)
(guix utils)
(guix git-download))
(define emacs-jtsx
(let ((commit "65efa5bded314e788fa6b3f5a367f4067f9d2727")
(revision "1"))
(package (name "emacs-jtsx")
(version (git-version "0.4.1" revision commit))
(source
(origin
(method git-fetch)
(uri (git-reference
(url "https://github.com/llemaitre19/jtsx.git")
(commit commit)))
(sha256
(base32
"177i3gljg19jsrfcrxnvi6h26g52lbzf3var6c30bv4lmfm7d8s7"))))
(build-system emacs-build-system)
(home-page
"https://github.com/llemaitre19/jtsx.git")
(synopsis "Extends Emacs JSX/TSX built-in support")
(description "jtsx is an Emacs package for editing JSX or TSX files. It provides jtsx-jsx-mode and jtsx-tsx-mode major modes implemented respectively on top of js-ts-mode and tsx-ts-mode, benefiting thus from the new built-in Tree-sitter feature.")
(license license:gpl3))))
(concatenate-manifests
(list
(packages->manifest
(list
;; Put custom package expressions here
emacs-jtsx))
(specifications->manifest
'(
;; Put package names of packages from guix here
"emacs"
"emacs-magit"))
See also:
Instead of directly writing a manifest.scm file and running all of
those commands to manage the guix profiles for my emacs environment, I
write my emacs configuration in one file that tangles into init.el
manifest.scm
and some scripts in $HOME/.emacs.d/bin/
.
The skeleton for the manifest file. Doesn’t have any packages by default, we use the org babel refs later in this file to fill it out
packages
is for strings of package names to install from guix repos.
package-requirements
, package-definitions
, and
package-expressions
are used for our own custom packages
We check it into our dotfiles to make bootstrapping easier.
;; -*- geiser-scheme-implementation: guile -*-
(use-modules (guix channels)
(guix inferior)
(srfi srfi-1)
;; Requirements for packages
(guix packages)
(guix transformations)
((guix licenses) #:prefix license:)
(guix build-system emacs)
(guix utils)
(guix download)
(guix git-download)
(gnu packages)
(gnu packages emacs)
(gnu packages emacs-xyz)
<<package-requirements>>
)
<<package-definitions>>
(concatenate-manifests
(list
(packages->manifest
(list
(make-glibc-utf8-locales
glibc
#:locales (list "en_US")
#:name "glibc-english-utf8-locales")
<<package-expressions>>
))
(specifications->manifest
'(
<<packages>>
))))
Essential Packages
"emacs"
Setup script. Commit to dotfiles repo to aid in bootstrapping.
mkdir -p $HOME/.emacs.d/guix
mkdir -p $HOME/.emacs.d/bin
guix pull --channels="$HOME/emacs-dotfiles/channels-lock.scm" \
--profile="$HOME/.emacs.d/guix/pull-profile"
$HOME/.emacs.d/guix/pull-profile/bin/guix package -m $HOME/emacs-dotfiles/manifest.scm -p $HOME/.emacs.d/guix/package-profile
Update guix and generate new lock file.
guix pull --profile=$HOME/.emacs.d/guix/pull-profile
$HOME/.emacs.d/guix/pull-profile/bin/guix describe --format=channels > $HOME/emacs-dotfiles/channels-lock.scm
Build guix from lock file.
guix pull -C $HOME/emacs-dotfiles/channels-lock.scm -p $HOME/.emacs.d/guix/pull-profile
Install the manifest with guix profile
# Pull guix to install our packages with if it isn't already installed
if [ ! -d $HOME/.emacs.d/guix/pull-profile ]; then
. ~/.emacs.d/bin/lock
fi
$HOME/.emacs.d/guix/pull-profile/bin/guix package -m $HOME/emacs-dotfiles/manifest.scm -p $HOME/.emacs.d/guix/package-profile
Command to use the emacs guix (useful for searching for packages)
$HOME/.emacs.d/guix/pull-profile/bin/guix $@
Launcher scripts for emacs and emacsclient
GUIX_PROFILE=$HOME/.emacs.d/guix/pull-profile
. "$GUIX_PROFILE/etc/profile"
GUIX_PROFILE=$HOME/.emacs.d/guix/package-profile
. "$GUIX_PROFILE/etc/profile"
emacs $@
GUIX_PROFILE=$HOME/.emacs.d/guix/pull-profile
. "$GUIX_PROFILE/etc/profile"
GUIX_PROFILE=$HOME/.emacs.d/guix/package-profile
. "$GUIX_PROFILE/etc/profile"
emacsclient $@
Delete old generations of both profiles and run guix gc
to free up
space. (You will still be able to reinstall the packages associated
with an old channel-lock.scm
file, but guix might have to compile
them)
guix pull --profile="~/.emacs.d/guix/pull-profile" --delete-generations
guix package --profile="~/.emacs.d/guix/package-profile" --delete-generations
"emacs-git-gutter"
(require 'git-gutter)
(global-git-gutter-mode +1)
(Highly recommend this package if you use jsx btw!)
(define emacs-jtsx
(let ((commit "65efa5bded314e788fa6b3f5a367f4067f9d2727")
(revision ""))
(package (name "emacs-jtsx")
(version (git-version "0.4.1" revision commit))
(source
(origin
(method git-fetch)
(uri (git-reference
(url "https://github.com/llemaitre19/jtsx.git")
(commit commit)))
(sha256
(base32
"177i3gljg19jsrfcrxnvi6h26g52lbzf3var6c30bv4lmfm7d8s7"))))
(build-system emacs-build-system)
(home-page
"https://github.com/llemaitre19/jtsx.git")
(synopsis "Extends Emacs JSX/TSX built-in support")
(description "jtsx is an Emacs package for editing JSX or TSX files. It provides jtsx-jsx-mode and jtsx-tsx-mode major modes implemented respectively on top of js-ts-mode and tsx-ts-mode, benefiting thus from the new built-in Tree-sitter feature.")
(license license:gpl3))))
emacs-jtsx
(defun jtsx-bind-keys-to-mode-map (mode-map)
"Bind keys to MODE-MAP."
(define-key mode-map (kbd "C-c C-j") 'jtsx-jump-jsx-element-tag-dwim)
(define-key mode-map (kbd "C-c j o") 'jtsx-jump-jsx-opening-tag)
(define-key mode-map (kbd "C-c j c") 'jtsx-jump-jsx-closing-tag)
(define-key mode-map (kbd "C-c j r") 'jtsx-rename-jsx-element)
(define-key mode-map (kbd "C-c <down>") 'jtsx-move-jsx-element-tag-forward)
(define-key mode-map (kbd "C-c <up>") 'jtsx-move-jsx-element-tag-backward)
(define-key mode-map (kbd "C-c C-<down>") 'jtsx-move-jsx-element-forward)
(define-key mode-map (kbd "C-c C-<up>") 'jtsx-move-jsx-element-backward)
(define-key mode-map (kbd "C-c C-S-<down>") 'jtsx-move-jsx-element-step-in-forward)
(define-key mode-map (kbd "C-c C-S-<up>") 'jtsx-move-jsx-element-step-in-backward)
(define-key mode-map (kbd "C-c j w") 'jtsx-wrap-in-jsx-element)
(define-key mode-map (kbd "C-c j u") 'jtsx-unwrap-jsx)
(define-key mode-map (kbd "C-c j d") 'jtsx-delete-jsx-node))
(defun jtsx-bind-keys-to-jtsx-jsx-mode-map ()
(jtsx-bind-keys-to-mode-map jtsx-jsx-mode-map))
(defun jtsx-bind-keys-to-jtsx-tsx-mode-map ()
(jtsx-bind-keys-to-mode-map jtsx-tsx-mode-map))
(add-hook 'jtsx-jsx-mode-hook 'jtsx-bind-keys-to-jtsx-jsx-mode-map)
(add-hook 'jtsx-tsx-mode-hook 'jtsx-bind-keys-to-jtsx-tsx-mode-map)
Note: If you want to install fonts, you must also install the fontconfig
package
"fontconfig"
"font-fira-code"
(set-face-attribute 'default nil :family "Fira Code")
Hey thanks for this !