Use Emacs-Next in Guix Home

2024-09-09
Forward links: related:Emacs related:Guix under:Blog Backward links:

Guix by its nature conduits a change to a package along the dependency graph, producing a working profile containing the new dependency sub-graph. In the profile there can be packages depending on different versions of a conceptually same package, which might works because ideally different sub-graphs are isolated.

For Emacs sadly, a newer Emacs instance does not always work well with packages compiled with previous Emacs versions.

The solution to this is of course build all used Emacs packages with new Emacs version.

Attempt that failed

It is mentioned that we can use with-input transformation or equivalent to accomplish this, see

  • Fredrik Salomonsson and Nicolas Goaziou and Simon Tournier and Maxim Cournoyer: Implement a wrapper so users can build the Emacs packages using a version of their choosing(Salomonsson et al. 2020)
  • Mekeor Melire and Liliana Marie Prikler and Simon Tournier: Emacs-Packages should contain native-compiled files(Melire, Prikler, and Tournier 2023)
  • Mekeor Melire and Liliana Marie Prikler and Maxim Cournoyer and Simon Tournier: Allow for easily rewriting Emacs packages to use emacs-next(Melire et al. 2023)

In my home configuration, I have all Emacs packages declared as simple services extending home-profile-service-type, so I came up with

(define-module (hiecaq home emacs)
  #:use-module (gnu services)
  #:use-module (gnu packages)
  #:use-module (gnu home services)
  #:use-module (guix transformations)
  #:use-module (guix gexp))

(define transform-with-emacs-next
  (options->transformation (list
                            '(with-input . "emacs-minimal=emacs-next-minimal")
                            '(with-input . "emacs=emacs-next")))

  (define-public services
    (map (lambda (serv)
           (if (service-value serv)
               (service
                (service-kind serv)
                (map transform-with-emacs-next (service-value serv)))
               serv))
         (list
          ;; emacs package services here
          )))

Which does not work well, so I looked for other solutions.

Second attempt

So by searching around I discovered (Joseph 2024), which gives a hint on using package-input-rewriting.

I also take inspiration from this configuration that I should try defining a service for Emacs, to both strengthen my Guix-fu and to solidify my configuration set-up.

It turns out to be pretty simple. Mostly I just need to follow how gnu/home/services/shells.scm does it.

First, define a base configuration:

(define-configuration/no-serialization home-emacs-configuration
  (emacs
   (package upstream:emacs)
   "Emacs to use.")
  (emacs-compiler
   (package upstream:emacs-minimal)
   "Emacs used for compiling packages.")
  (packages
   (list '())
   "List of Emacs packages to use.")
  (configs
   (alist '())
   "Emacs configuration files."))

This is the actual configuration used for the very home-emacs-service-type service.

To override Emacs used in the packages, I define the following helper function. The emacs package used for compilation is put into inputs, and is referred from that during Emacs package compilation. There are actually 3 inputs needs to be overridden:

(define (home-emacs-transformed-package config)
  (package-input-rewriting
   `((,upstream:emacs-minimal
      . ,(home-emacs-configuration-emacs-compiler config))
     (,upstream:emacs-no-x
      . ,(home-emacs-configuration-emacs config))
     (,upstream:emacs
      . ,(home-emacs-configuration-emacs config)))))

Then I define a pseudo getter function for the transformed packages, plus the actual Emacs instance to use:

(define (home-emacs-profile config)
  `(,(home-emacs-configuration-emacs config)
    ,@(map (home-emacs-transformed-package config)
           (home-emacs-configuration-packages config))))

The next thing is to define the extension. All services extends home-emacs-service-type will provide the following record:

(define-configuration/no-serialization home-emacs-extension
  (packages
   (list '())
   "Extra list of Emacs packages to use.")
  (configs
   (alist '())
   "Extra Emacs configuration files."))

How the actual single service of home-emacs-service-type consumes these provided extension record is defined as the following helper function.

It takes the home-emacs-configuration, which is declared when the single service of home-emacs-service-type is defined, and the list of home-emacs-extension records from the services extending home-emacs-service-type. It simply just join packages and configs fields of these 2 records kind together.

(define (home-emacs-extensions original-config extension-configs)
  (let ((append-fields
         (lambda (config-getter extension-getter)
           (append (config-getter original-config)
                   (append-map extension-getter extension-configs)))))
    (home-emacs-configuration
     (inherit original-config)
     (packages (append-fields home-emacs-configuration-packages
                              home-emacs-extension-packages))
     (configs (append-fields home-emacs-configuration-configs
                             home-emacs-extension-configs)))))

Finally, the actual service type is

(define home-emacs-service-type
  (service-type
   (name 'home-emacs)
   (extensions
    (list (service-extension home-xdg-configuration-files-service-type
                             home-emacs-configuration-configs)
          (service-extension home-profile-service-type
                             home-emacs-profile)))
   (compose identity)
   (extend home-emacs-extensions)
   (default-value (home-emacs-configuration))
   (description #f)))

Besides using the helper functions above, the only other thing worth noting is that compose is using identity, which means the list of home-emacs-extension records are passed as-is.

How this works in reality

First you'll want a home-emacs-service-type service that declare to use emacs-next.

(service home-emacs-service-type
         (home-emacs-configuration
          (emacs upstream:emacs-next)
          (emacs-compiler upstream:emacs-next-minimal)))

To declare a package usage, simply

(simple-service
 'home-emacs-god-mode
 home-emacs-service-type
 (home-emacs-extension
  (packages
   (list
    (specification->package
     "emacs-god-mode")))))

Reference

Joseph, Marie. 2024. “Juix/Home/Services.Scm.” 2024. https://git.trees.st/Marie-Joseph/juix/src/branch/main/juix/home/services.scm.
Melire, Mekeor, Liliana Marie Prikler, and Simon Tournier. 2023. “Emacs-Packages Should Contain Native-Compiled Files.” July 12, 2023. https://issues.guix.gnu.org/issue/64586.
Melire, Mekeor, Liliana Marie Prikler, Maxim Cournoyer, and Simon Tournier. 2023. “Allow for Easily Rewriting Emacs Packages to Use Emacs-next.” June 6, 2023. https://issues.guix.gnu.org/issue/63920.
Salomonsson, Fredrik, Nicolas Goaziou, Simon Tournier, and Maxim Cournoyer. 2020. “Implement a Wrapper so Users Can Build the Emacs Packages Using a Version of Their Choosing.” June 6, 2020. https://issues.guix.gnu.org/issue/41732.