Skip to content

Instantly share code, notes, and snippets.

@noah-evans
Last active September 10, 2024 04:05
Show Gist options
  • Save noah-evans/745c5dabf92fa62078c8d7a6952d790d to your computer and use it in GitHub Desktop.
Save noah-evans/745c5dabf92fa62078c8d7a6952d790d to your computer and use it in GitHub Desktop.
Using the guix package manager for a cross-distro, reproducible emacs environment.

Using the guix package manager for a cross-distro, reproducible emacs environment

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.

Reproducible guix profile explanation

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 with guix 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.

Guix Profiles

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:

Guix Pull/Channels

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:

Manifest

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:

Example tangled configuration

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/.

Manifest Skeleton

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"

Scripts

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

Package example

"emacs-git-gutter"
(require 'git-gutter)
(global-git-gutter-mode +1)

Custom package example

(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)

Installing a font

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")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment