Subject: [bug#64620] [PATCH v2] gnu: home: Add home-emacs-service-type.

From: [email-masked]
Date: Fri, 7 Mar 2025 00:40:12 -0500

From: Kierin Bell <[email-masked]>

* gnu/home/services/emacs.scm: New file.
* guix/formatters/elisp.scm: New file.
* guix/read-print.scm (%newline-forms): Add newline forms for
'home-emacs-packages-service-type'.
* guix/scripts/home/import.scm (generate-emacs-packages-configuration+modules)
(%files+configurations-alist): Add importer for Emacs init files.
* gnu/local.mk (GNU_SYSTEM_MODULES): Add new file.
* tests/home/services/emacs.scm: New tests file.
* tests/formatters/elisp.scm: New tests file.
* Makefile.am (SCM_TESTS): Add new test files.
* doc/guix.texi (Emacs Home Services): New node.

Change-Id: I29d32d7facea1490caef023aac299f456b579fc9
---

This is version 2 of patch for the 'home-emacs-service-type', building
on work by ( and David Wilson (https://issues.guix.gnu.org/58693,
https://issues.guix.gnu.org/60753, https://issues.guix.gnu.org/62549).

I have split what was previously the 'home-emacs-service-type' into two
service types, a basic 'home-emacs-service-type' and a
'home-emacs-packages-service-type' that extends the basic service type
and provides the extra bells and whistles to configure Emacs in more
heavily-abstracted Scheme.

Addressing Ludo's concerns, I have moved the Elisp pretty printer out of
(guix read-print) into a new module named (guix formatters elisp).
Currently, it's just a modified version of 'pretty-print-with-comments'
adapted for Elisp serialization. It would be better to have a general
purpose monadic formatting library in Guix for this sort of thing in the
future, but for now this works well.

Also as suggested by Ludo', I have removed the Elisp reader extension
that allowed you to write Elisp read syntax directly within Scheme. This
required patching the custom reader from (guix read-print), which is no
longer needed.

To hopefully lower the barrier to entry, integration for 'guix home
import' has been added, and it should do a decent job at generating
configurations from existing Emacs init files.

[ There is a limitation with (guix scripts home import) that in order to
import an Emacs configuration file that is in a subdirectory of the HOME
directory (e.g., '~/.config/emacs'), a matching directory must exist in
the target directory passed to 'guix home import'. Otherwise, it will
try to copy the original file into a non-existing directory and
'copy-file' will signal an error. I did not patch that here, since it
may be a design decision for Guix Home that is out of scope. ]

Unit tests have also been added for the Elisp pretty printer and the
'guix home import' integration. As we discussed before, unit testing the
actual services and their serialization of configuration records to
Elisp is complicated, as there is no standard way of unit testing
services yet. But I have battle tested these services by managing my own
large Emacs init file like this.

Mostly following the original patches from ( and David Wilson, the basic
'home-emacs-service-type' service lets you:

* specify a list of file-likes and s-expressions to serialize to the
  Emacs user initialization and early initialization files (file-likes
  are spliced in verbatim and s-expressions specified in Scheme are
  pretty-printed as Elisp),

* change the 'user-emacs-directory' (in a way that updates variables
  from the Emacs early init process that would not be properly updated
  simply by setting the variable) and populate it with files,

* specify whether to load customizations created with the Emacs
  customization interface, which Emacs is directed to put into a
  separate 'custom.el' file so that it does not try to modify the
  read-only 'init.el' file created by the service.

* create Emacs servers via Shepherd services (and optionally specify
  separate initialization files and user directories for these servers).

* and, specify a version of Emacs to install and packages to compile
  with that version of Emacs [if they are Emacs packages] and install.

[ The last one, native compilation, is disabled by default, since
compiling Emacs packages for anything other than 'emacs-minimal' is
uncharted/untested territory. In particular, 'emacs-buttercup' (and
packages that depend on it) fails to build due to issues with its test
stage that I haven't been able to pin down. This should probably be
fixed in a follow-up patch to that package. ]

The other service type, the 'home-emacs-packages-service-type', lets you
configure packages (and Emacs itself) using configuration record fields
largely modeled after the keywords of the Emacs 'use-package' macro.

The "serializer" used to convert these records into Elisp can be user
defined. Currently there are two predefined serializers: one that uses
'use-package' forms, and another that just serializes basic Elisp. But
defining custom serializers is straightforward, and there is an
escape-hatch field ('extra-keywords') intended for use by custom
serializers. This means that while in practice there are suggested
semantics for the configuration record fields used by the service,
anyone can use them however they want.

Here is an example configuration that uses the basic
'home-emacs-service-type':

--8<---------------cut here---------------start------------->8---
(use-modules (gnu home)
             (gnu services)
             (gnu packages)
             (guix gexp)
             (gnu home services)
             (gnu home services emacs)
             (gnu packages emacs-xyz))

(define %gnus-init-file
        (elisp-file
         "gnus.el"
         (elisp*
          (setopt gnus-select-method '(nnnil "")
                  gnus-secondary-select-methods '((nnml ""))
                  mail-sources
                  '((imap :server "mail.example.net"
                          :user "[email-masked]"
                          :port 993
                          :stream tls))))))

(home-environment
  (services
   (list
    (service
     home-emacs-service-type
     (home-emacs-configuration
      (user-emacs-directory "~/.local/state/emacs/")
      (packages (list emacs-bbdb))
      (init-file
       (cons*
        (local-file "my-init.el")        ; Include contents of a file.
        (elisp*
         (setopt gnus-init-file (locate-user-emacs-file "gnus.el")
                 mail-user-agent 'gnus-user-agent
                 read-mail-command (function gnus)
                 smtpmail-servers-requiring-authorization
                 "mail\\.example\\.net"
                 smtpmail-smtp-server "mail.example.net"
                 smtpmail-smtp-service 587
                 smtpmail-smtp-user "[email-masked]"
                 message-send-mail-function (function smtpmail-send-it)
                 message-signature-file
                 (locate-user-emacs-file "signature")
                 bbdb-user-mail-address-re
                 (regexp-opt "[email-masked]")
                 bbdb-auto-notes-ignore-messages
                 `(("From" . ,bbdb-user-mail-address-re)))
         (keymap-global-set "C-c n" (function gnus))
         (bbdb-initialize 'gnus 'message 'anniv)
         (bbdb-mua-auto-update-init 'gnus 'message))))
      (user-files                ; Files for the 'user-emacs-directory'.
       `(("gnus.el" . ,%gnus-init-file)
         ("signature" . ,(local-file "signature"))))
      (servers
       (list
        (emacs-server
         (name "main")
         (environment-variables
          #~(cons* #$(string-append "WAYLAND_DISPLAY="
                                    (or (getenv "WAYLAND_DISPLAY")
                                        "wayland-1"))
                   #$(string-append "DISPLAY="
                                    (or (getenv "DISPLAY") ":0"))
                   (default-environment-variables)))))))))))
--8<---------------cut here---------------end--------------->8---

And here is an equivalent configuration using the
'home-emacs-packages-service-type':

--8<---------------cut here---------------start------------->8---
...
(home-environment
  (services
   (list
    (service
     home-emacs-service-type
     (home-emacs-configuration
      (user-emacs-directory "~/.local/state/emacs/")
      (init-file
       (list (local-file "my-init.el")))
      (servers
       (list
        (emacs-server
         (name "main")
         (environment-variables
          #~(cons* #$(string-append "WAYLAND_DISPLAY="
                                    (or (getenv "WAYLAND_DISPLAY")
                                        "wayland-1"))
                   (default-environment-variables))))))))
    (service
     home-emacs-packages-service-type
     (home-emacs-packages-configuration
      (packages
       (list
        (emacs-package
          (name 'gnus)
          (user-files
           `(("gnus.el" . ,%gnus-init-file)
             ("signature" . ,(local-file "signature"))))
          (keys-global '(("C-c n" . gnus)))
          (options
           `((gnus-init-file
              . ,(elisp (locate-user-emacs-file "gnus.el")))
             (mail-user-agent . gnus-user-agent)
             (read-mail-command . gnus))))
        (emacs-package
          (name 'smtpmail)
          (options '((smtpmail-servers-requiring-authorization
                      . "mail\\.example\\.net")
                     (smtpmail-smtp-server . "mail.example.net")
                     (smtpmail-smtp-service . 587)
                     (smtpmail-smtp-user . "[email-masked]"))))
        (emacs-package
          (name 'message)
          (options `((message-send-mail-function . smtpmail-send-it)
                     (message-signature-file
                      . ,(elisp (locate-user-emacs-file
                                 "signature"))))))
        (emacs-package
          (name 'bbdb)
          (package emacs-bbdb)
          (options
           `((bbdb-user-mail-address-re
              . ,(elisp (regexp-opt "[email-masked]")))
             (bbdb-auto-notes-ignore-messages
              . ,(elisp `(("From"
                           . ,bbdb-user-mail-address-re))))))
          (extra-init
           (elisp*
            (bbdb-initialize 'gnus 'message 'anniv)
            (bbdb-mua-auto-update-init 'gnus 'message)))))))))))
--8<---------------cut here---------------end--------------->8---

I won't deny that writing Elisp in Scheme like this may not be for
everyone, but I've come to prefer it for basic configuration. I've found
it helpful to add an 'extra-init-files' field to the 'emacs-package'
record type that allows you to specify additional Elisp files to symlink
and load for a given package. This encourages a nice balance between
basic configuration in Scheme, like setting options, and heavy-lifting,
like deffuns and advice, in separate files. Of course, we could also
create an Emacs package that makes managing Elisp in Scheme easier, too.

Thanks for all of the feedback, and apologies that it took so long to
take the next step!

 Makefile.am                   |    4 +
 doc/guix.texi                 | 1155 ++++++++++++
 gnu/home/services/emacs.scm   | 3101 +++++++++++++++++++++++++++++++++
 gnu/local.mk                  |    2 +
 guix/formatters/elisp.scm     |  918 ++++++++++
 guix/read-print.scm           |   13 +
 guix/scripts/home/import.scm  |   25 +-
 tests/formatters/elisp.scm    |  168 ++
 tests/home/services/emacs.scm |  306 ++++
 9 files changed, 5691 insertions(+), 1 deletion(-)
 create mode 100644 gnu/home/services/emacs.scm
 create mode 100644 guix/formatters/elisp.scm
 create mode 100644 tests/formatters/elisp.scm
 create mode 100644 tests/home/services/emacs.scm

diff --git a/Makefile.am b/Makefile.am
index c668b96a37..8e400c69c8 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -20,6 +20,8 @@
 # Copyright © 2023 Clément Lassieur <[email-masked]>
 # Copyright © 2023, 2024 Wilko Meyer <[email-masked]>
 # Copyright © 2024 gemmaro <[email-masked]>
+# Copyright © 2023 Wilko Meyer <[email-masked]>
+# Copyright © 2023, 2025 Kierin Bell <[email-masked]>
 #
 # This file is part of GNU Guix.
 #
@@ -552,6 +554,7 @@ SCM_TESTS =					\
   tests/elm.scm				\
   tests/elpa.scm				\
   tests/file-systems.scm			\
+  tests/formatters/elisp.scm			\
   tests/gem.scm				\
   tests/gexp.scm				\
   tests/git.scm					\
@@ -565,6 +568,7 @@ SCM_TESTS =					\
   tests/hexpm.scm				\
   tests/home-import.scm				\
   tests/home-services.scm			\
+  tests/home/services/emacs.scm			\
   tests/http-client.scm				\
   tests/import-git.scm				\
   tests/import-github.scm			\
diff --git a/doc/guix.texi b/doc/guix.texi
index 6844470ce2..8b552dfef2 100644
--- a/doc/guix.texi
+++ b/doc/guix.texi
@@ -138,6 +138,7 @@
 Copyright @copyright{} 2025 Sören Tempel@*
 Copyright @copyright{} 2025 Rostislav Svoboda@*
 Copyright @copyright{} 2025 Zacchaeus@*
+Copyright @copyright{} 2023-2025 Kierin Bell@*
 
 Permission is granted to copy, distribute and/or modify this document
 under the terms of the GNU Free Documentation License, Version 1.3 or
@@ -462,6 +463,7 @@ Top
 * SSH: Secure Shell.            Setting up the secure shell client.
 * GPG: GNU Privacy Guard.       Setting up GPG and related tools.
 * Desktop: Desktop Home Services.  Services for graphical environments.
+* Emacs: Emacs Home Services.   Services for configuring Emacs.
 * Guix: Guix Home Services.     Services for Guix.
 * Fonts: Fonts Home Services.   Services for managing User's fonts.
 * Sound: Sound Home Services.   Dealing with audio.
@@ -47660,6 +47662,7 @@ Home Services
 * SSH: Secure Shell.            Setting up the secure shell client.
 * GPG: GNU Privacy Guard.       Setting up GPG and related tools.
 * Desktop: Desktop Home Services.  Services for graphical environments.
+* Emacs: Emacs Home Services.   Services for configuring Emacs.
 * Guix: Guix Home Services.     Services for Guix.
 * Fonts: Fonts Home Services.   Services for managing User's fonts.
 * Sound: Sound Home Services.   Dealing with audio.
@@ -49174,6 +49177,1158 @@ Desktop Home Services
 @command{startx} used.  Default value is @code{(xorg-configuration)}.
 @end defvar
 
+@node Emacs Home Services
+@subsection Emacs Home Services
+
+The @code{(gnu home services emacs)} module provides services for
+configuring the GNU Emacs extensible text editor.
+
+@cindex Elisp expressions, for Emacs Home services
+Emacs is configured by providing @dfn{initialization files} that contain
+@dfn{s-expressions} written in @dfn{Emacs Lisp} (abbreviated as
+@dfn{Elisp}) which are evaluated when Emacs is started (@pxref{Init
+File,,, emacs, The GNU Emacs Manual}).
+
+The @code{(gnu home services emacs)} module introduces a mechanism for
+specifying s-expressions that should be serialized as Elisp and
+evaluated by Emacs: @dfn{Elisp expressions}.  Elisp expressions have
+their own data type (see @code{elisp?}), and they must be created by
+using @code{elisp} forms, or by using other functions provided by the
+module for constructing them.  Whenever the term ``Elisp expression''
+occurs in this documentation, it is an indication that Elisp expressions
+of this type should be used.
+
+In many ways, Elisp expressions are similar to G-expressions
+(@pxref{G-Expressions}).  Elisp expressions can in fact be thought of as
+an abstraction around G-expressions.  After all, before any Elisp
+expression can be serialized to a file by a service, it must first be
+transformed into a G-expression so that a derivation can be generated
+(@pxref{Derivations}).
+
+For this reason, any value that is a valid input for a G-expression can
+be referenced within an Elisp expression (see @code{unelisp} and
+@code{unelisp-splicing} below).  Data types that ``compile'' and are
+specially substituted in G-expressions, such as file-like objects
+(@pxref{G-Expressions, file-like objects}), will be substituted in the
+same exact way when they are referenced within Elisp expressions.  Even
+G-expressions themselves can be embedded within Elisp expressions.
+
+On the other hand, when Elisp expressions are referenced manually within
+G-expressions (e.g., with @code{ungexp}), some of the expressive power
+of Elisp expressions is lost, as explained below: comments, newlines,
+and page-breaks are stripped.
+
+@defmac elisp @var{exp}
+Return an Elisp expression containing @var{exp}.
+
+Expressions within @var{exp} are constants rather than expressions that
+are evaluated for their Scheme values---as if the expressions were
+quoted using the @code{quote} syntax---unless they are ``unquoted'' with
+one of the following two forms:
+
+@table @code
+@item (unelisp @var{exp})
+Include the value of @var{exp} in an @code{elisp} form.
+
+@var{exp} is an s-expression, given in Scheme, that is evaluated, and
+the resulting value is included within the containing form.  Any values
+that may appear within G-expressions are valid, and any substitutions
+that would be made when ``compiling'' a G-expression will also be made
+when the resulting Elisp expression is serialized to a file.
+
+If the result of evaluating @var{exp} is a list, it is traversed and all
+relevant substitutions are similarly performed.
+
+If the result of evaluating @var{exp} is another Elisp expression, its
+contents are inserted, with the relevant references included as above.
+
+@item (unelisp-splicing @var{lst})
+Like the above, but splices the contents of @var{lst} inside the
+containing expression (which must itself be a list).
+@end table
+
+Additionally, the following forms allow for the inclusion of comments
+and whitespace into Elisp expressions:
+
+@table @code
+@item (unelisp-comment @var{comment})
+Insert a comment containing the string @var{comment} into the containing
+expression.
+
+@var{comment} must be a proper string that begins with @samp{;} and ends
+with a newline character.
+
+When the containing Elisp expression is serialized to an Elisp file (see
+@code{elisp-file}), the comment is pretty-printed as it occurs.
+However, when an Elisp expression is referenced within a G-expression
+manually (e.g., using the @code{ungexp} syntax), all comments specified
+with these forms are lost.  This is because comments cannot normally be
+``compiled'' into a substitution while lowering a G-expression.
+
+@item (unelisp-newline)
+Insert a newline into the containing expression.
+
+When an Elisp expression is serialized to an Elisp file, newlines are
+inserted where they occur.  But, as with @code{unelisp-comment},
+newlines specified using this syntax are removed when an Elisp
+expression is referenced manually within a G-expression.
+
+@item (unelisp-page-break)
+Insert a page-break character into the containing expression.
+
+When an Elisp expression is serialized to an Elisp file, page-break
+characters are inserted where they occur, but, again, they are removed
+when an Elisp expression is manually referenced within a G-expression.
+@end table
+@end defmac
+
+Each Elisp expression can only contain a single s-expression.  To
+specify multiple s-expressions, we must use a list of Elisp expressions.
+The macro @code{elisp*} exists to do this more succinctly.
+
+For instance, this:
+
+@lisp
+(list
+ (elisp (require 'window))
+ (elisp (defun my--get-other-window ()
+          (get-mru-window nil nil t)))
+ (elisp (setopt other-window-scroll-default
+                (function my--get-other-window))))
+@end lisp
+
+@noindent
+is equivalent to this:
+
+@lisp
+(elisp*
+ (require 'window)
+ (defun my--get-other-window ()
+   (get-mru-window nil nil t))
+ (setopt other-window-scroll-default
+         (function my--get-other-window)))
+@end lisp
+
+@defmac elisp* @var{exps} @dots{}
+Return a list of Elisp expressions containing @var{exps}.
+@end defmac
+
+@deffn {Procedure} elisp? obj
+Return true if @var{obj} is an Elisp expression object.
+@end deffn
+
+@deffn {Procedure} elisp->sexp exp
+Return an s-expression containing the contents of Elisp expression
+@var{exp}.
+@end deffn
+
+@deffn {Procedure} sexp->elisp sexp
+Return an Elisp expression object containing @var{sexp}.
+@end deffn
+
+@cindex Elisp files, for Emacs Home services
+Once we have some Elisp expressions, we need to be able to serialize
+them to an Elisp file.  Usually, we provide Elisp expressions within
+configuration fields for Emacs Home services, which automatically
+serialize them to the appropriate Emacs initialization files.  However,
+we can also serialize Elisp expressions directly to arbitrary files
+ourselves.  The @code{elisp-file} procedure takes Elisp expressions and
+returns a file-like object ensuring that the expressions will be
+pretty-printed as Elisp---comments, newlines and all.
+
+@deffn {Procedure} elisp-file name exps [#:special-forms ()]
+Return an object representing the store file @var{name}, an Emacs Lisp
+file that contains @var{exps}, a list of Elisp expression objects or
+G-expressions.
+
+Custom indentation rules can be specified with @var{special-forms}, an
+association list where each entry is of the form:
+
+@lisp
+(@var{symbol} . @var{indent})
+@end lisp
+
+@noindent
+When @var{symbol} occurs at the beginning of a list in an expression in
+@var{exps}, the first @var{indent} expressions after @var{symbol} are
+indented as arguments and the remainder are indented as body
+expressions, as if @var{indent} was the value of the
+@code{lisp-indent-function} symbol property for @var{symbol} in Emacs
+(@pxref{Indenting Macros,,,elisp,The Emacs Lisp Manual}).  As in Emacs,
+argument expressions, if they cannot be pretty-printed on the same line
+as @var{symbol}, are indented 4 columns beyond the base indentation of
+the enclosing list, and body expressions are indented 2 columns beyond
+the base indentation.
+
+This is the declarative counterpart of @code{elisp-file*}.
+@end deffn
+
+@deffn {Procedure} elisp-file* name exps [#:special-forms ()]
+Return as a monadic value a derivation that builds an Elisp file named
+@var{name} containing the expressions in @var{exps}, a list of Elisp
+expression objects or G-expressions.
+
+This is the monadic counterpart of @code{elisp-file}, which see for a
+description of @var{special-forms}.
+@end deffn
+
+@deffn {Procedure} elisp-file? obj
+Return true if @var{obj} is an Elisp file object.
+@end deffn
+
+There are two service types for configuring Emacs.  The fundamental
+service type for configuring Emacs is the
+@code{home-emacs-service-type}.  The
+@code{home-emacs-packages-service-type} extends the former by providing
+a useful layer of abstraction for the configuration of Emacs packages
+and any associated options and keybindings in Scheme.
+
+@cindex Emacs, Home service
+@cindex Emacs, configuration
+@defvar home-emacs-service-type
+This is the basic service type for configuring Emacs.  Its value is a
+@code{home-emacs-configuration} object.
+
+Here is a sample Guix Home configuration that utilizes the
+@code{home-emacs-service-type}:
+
+@lisp
+(use-modules (gnu home)
+             (gnu services)
+             (guix gexp)
+             (gnu home services emacs)
+             (gnu packages emacs-xyz))
+
+(home-environment
+  (services
+   (list
+    (service
+     home-emacs-service-type
+     (home-emacs-configuration
+      (user-emacs-directory "~/.local/state/emacs/")
+      (packages (list emacs-orderless))
+      (init-file
+       (cons*
+        (elisp
+         (unelisp-comment
+          ";; Copyright (C) 2025 Free Software Foundation, Inc.\n"))
+        (local-file "my-init.el")   ; Include contents of a file.
+        (elisp*
+         (setopt confirm-kill-emacs 'y-or-n-p
+                 visible-bell #t
+                 initial-scratch-message #f)
+         (keymap-global-set "C-x q" (function bury-buffer)
+                            "C-x C-b" (function ibuffer))
+         (setq-default indent-tabs-mode nil)
+         (tool-bar-mode -1)
+         (delete-selection-mode 1)
+         (setopt completion-styles '(orderless basic)
+                 completion-category-overrides
+                 '((file (styles basic partial-completion)))))))
+      (servers
+       (list
+        (emacs-server
+         (name "main")
+         (environment-variables
+          #~(cons* #$(string-append "WAYLAND_DISPLAY="
+                                    (or (getenv "WAYLAND_DISPLAY")
+                                        "wayland-1"))
+                   #$(string-append "DISPLAY="
+                                    (or (getenv "DISPLAY") ":0"))
+                   (default-environment-variables)))))))))))
+@end lisp
+
+@noindent
+The configuration above will install and configure Emacs, additionally
+installing and configuring the @code{emacs-orderless} package.
+
+This will configure Emacs by creating an @file{init.el} file in the
+Emacs configuration directory that has a copyright header comment, the
+contents of the @file{my-init.el} file, and Elisp s-expressions that set
+up some variables, keybindings, and global minor modes.  Additionally,
+it will set the Emacs user directory to @file{~/.local/state/emacs/},
+doing some things behind the scenes to ensure that any variables set
+during the Emacs early initialization process are updated appropriately.
+Finally, it will start an Emacs server called ``main'' using a Shepherd
+Home service (@pxref{Shepherd Home Service}), and set up some
+environment variables to ensure that the Emacs server works as expected.
+
+The @code{home-emacs-service-type} can be extended using the
+@code{home-emacs-extension} record type.  Here is an example:
+
+@lisp
+...
+(define %gnus-init-file
+        (elisp-file
+         "gnus.el"
+         (elisp*
+          (setopt gnus-select-method '(nnnil "")
+                  gnus-secondary-select-methods '((nnml ""))
+                  mail-sources
+                  '((imap :server "mail.example.net"
+                          :user "user@@example.net"
+                          :port 993
+                          :stream tls))))))
+
+(home-environment
+ (services
+  (list
+   ...
+   (simple-service
+    'emacs-mail-service
+    home-emacs-service-type
+    (home-emacs-extension
+     (init-file
+      (elisp*
+       (setopt gnus-init-file (locate-user-emacs-file "gnus.el")
+               mail-user-agent 'gnus-user-agent
+               read-mail-command (function gnus)
+               smtpmail-servers-requiring-authorization
+               "mail\\.example\\.net"
+               smtpmail-smtp-server "mail.example.net"
+               smtpmail-smtp-service 587
+               smtpmail-smtp-user "user@@example.net"
+               message-send-mail-function (function smtpmail-send-it)
+               message-signature-file
+               (locate-user-emacs-file "signature"))))
+     (user-files
+      `(("gnus.el" . ,%gnus-init-file)
+        ("signature" . ,(local-file "signature")))))))))
+@end lisp
+
+@noindent
+The configuration above extends the Emacs home service by configuring
+the Emacs Gnus Newsreader (@pxref{Top,,,Gnus,The Gnus Newsreader}) and
+the @code{smtpmail} and @code{message} packages so that Emacs can send
+and receive mail.
+
+Note that some users may prefer to configure these packages using the
+the @code{home-emacs-packages-service-type}, which see for an example of
+such a configuration.
+@end defvar
+
+The record types for configuring the Emacs Home service type are
+described in detail below.
+
+@deftp {Data Type} home-emacs-configuration
+Available @code{home-emacs-configuration} fields are:
+
+@table @asis
+@item @code{emacs} (default: @code{emacs}) (type: package)
+The package providing the @file{/bin/emacs} command.
+
+@item @code{packages} (default: @code{()}) (type: list-of-file-likes)
+A list of additional Emacs-related packages to install.
+
+@item @code{user-emacs-directory} (default: @code{"~/.config/emacs/"}) (type: string)
+Directory beneath which additional Emacs user files are placed.  By
+default, this is also the directory that contains the @file{init.el} and
+@file{early-init.el} Emacs initialization files, but you can change this
+field to specify any directory of your choosing; initialization files
+generated by this service will still be loaded.
+
+@item @code{native-compile?} (default: @code{#f}) (type: boolean)
+Whether to enable native-compilation of Emacs packages by building them
+with the Emacs specified by the @code{emacs} field rather than
+@code{emacs-minimal}.
+
+@item @code{indent-forms} (default: @code{()}) (type: alist)
+An association list of symbols and indentation rules.  Each entry is of
+the form (@var{symbol} .  @var{indent}), where @var{symbol} is a symbol
+and @var{indent} is an integer.
+
+When @var{symbol} occurs at the beginning of a list in an Emacs Lisp
+file, the first @var{indent} expressions are indented as arguments and
+the remainder as body expressions, as if @var{indent} was supplied as
+the @code{lisp-indent-function} symbol property for @var{symbol} in
+Emacs.  Argument expressions are either printed on the same line as
+@var{symbol} or indented 4 columns beyond the base indentation of the
+enclosing list, and body expressions are indented 2 columns beyond the
+base indentation.
+
+@item @code{load-custom?} (default: @code{#t}) (type: boolean)
+Whether to load customizations created with the Emacs customization
+interface.  Because all configuration files created by this service are
+effectively read-only, the service modifies the default behavior of
+Emacs so that customizations are always saved in a separate
+@file{custom.el} file, which will be loaded when Emacs is initialized if
+this field is true.
+
+@item @code{early-init-file} (default: @code{()}) (type: list-of-elisp-gexp-or-file-likes)
+A list of Elisp expressions, G-expressions, or file-like objects to
+serialize to the Emacs early init file, the @file{early-init.el} file in
+the appropriate Emacs configuration directory.  For file-like objects
+that represent actual files in the store (for example, those returned by
+@code{local-file} or @code{plain-file}), the contents of each file are
+serialized.  For other file-like objects (for example, @code{package}
+objects), the output file name of the object in the store is serialized,
+as if it were referenced in an Elisp expression or G-expression.
+
+The Emacs home service automatically serializes some s-expressions to
+the early initialization file in order to ensure that the Emacs user
+directory is properly set according to the @code{user-emacs-directory}
+field.  Any expressions specified in this field are serialized before
+the automatically added expressions.  This comes with a trade-off: one
+the one hand, you can leverage this fact to insert header comments or a
+license preamble at the beginning of the file, where it would be
+expected, but on the other hand, the Emacs user initialization directory
+will not be set until after any s-expressions specified in this field
+have already been evaluated.
+
+@item @code{init-file} (default: @code{()}) (type: list-of-elisp-gexp-or-file-likes)
+A list additional of Elisp expressions, G-expressions, or file-like
+objects to serialize to the Emacs user initialization file, the
+@file{init.el} file in the appropriate Emacs configuration directory.
+For file-like objects that represent actual files in the store (for
+example, those returned by @code{local-file} or @code{plain-file}), the
+contents of each file are serialized.
+
+As with the @code{early-init-file} field, some expressions are
+automatically serialized to the Emacs user initialization file by the
+Emacs home service, particularly those to set up the @file{custom.el}
+file (see the @code{load-custom?} field).  Expressions specified using
+this field are serialized before any automatically added expressions.
+This means that you can specify header comments or license preambles
+that will occur at the beginning of the file, but that the
+@file{custom.el} file will be loaded after all expressions in this field
+are evaluated.
+
+@item @code{user-files} (default: @code{()}) (type: alist)
+An association list of filenames and file-like objects specifying files
+to create in the Emacs user directory.  For each entry, a file with the
+given filename will be created with the contents of the file-like
+object.  If a list of file-like objects is given for an entry, the new
+file will contain the combined text contents of all of the file-like
+objects in the list.  This field can be used to add configuration files
+for Emacs that should not be automatically loaded when Emacs is
+initialized.  Note that the Emacs user directory, which can be specified
+using the @code{user-emacs-directory} field, may not be the same as the
+directory containing the Emacs user initialization file.
+
+@item @code{servers} (default: @code{()}) (type: list-of-emacs-servers)
+A list of configurations for Emacs servers.
+
+@end table
+
+@end deftp
+
+@deftp {Data Type} emacs-server
+Available @code{emacs-server} fields are:
+
+@table @asis
+@item @code{name} (type: string)
+A string naming the server.  Users will subsequently be able to start
+the new server by using the command @code{herd start emacs-@var{name}}.
+To create Emacs client frames for the sever, users can use commands
+like: @code{emacsclient --create-frame --socket-name=@var{name}}.
+Because this string is meant for use in shell commands (and filenames),
+it should not contain any characters other than letters and digits and
+the characters @samp{-}, @samp{_}, and @samp{.}.
+
+@item @code{inherit-user-directory?} (default: @code{#t}) (type: boolean)
+Whether the server should share its Emacs user directory with that of
+the Emacs Home service.  When false, the server will use a subdirectory
+of the one used by the service for its own user directory.  When true
+(the default), the @code{user-emacs-directory} Emacs variable for the
+server will be set to that of the Emacs Home service, but the server
+will still load its own @file{early-init.el} and @file{init.el} files.
+See the @code{inherit-init-files?} field for how to inherit
+configuration from other Emacsen.
+
+@item @code{inherit-init-files?} (default: @code{#t}) (type: boolean)
+Whether to load the default configuration used by the Emacs Home
+service, that is, the initialization expressions specified by the
+@code{home-emacs-configuration} value for the Emacs Home service.  These
+are loaded in addition to any configuration specified for this specific
+server.
+
+Note that if this field is true and @code{inherit-user-directory?} is
+false, duplicate copies of of any files specified by the
+@code{user-files} field of the @code{home-emacs-configuration} value for
+the service are created in the Emacs user directory for the server.
+This ensures that any references to those files in the inherited
+configuration expressions will not fail in unexpected ways.
+
+@item @code{auto-start?} (default: @code{#t}) (type: boolean)
+Whether to start the server automatically.
+
+@item @code{debug?} (default: @code{#f}) (type: boolean)
+Whether to enable the Emacs Lisp debugger for errors in the
+initialization files of the server.
+
+@item @code{shepherd-requirements} (default: @code{()}) (type: list-of-symbols)
+A list of symbols specifying Shepherd services that must be started
+before the service for the Emacs server can be started (@pxref{Defining
+Services,,, shepherd,The GNU Shepherd Manual}).
+
+@item @code{environment-variables} (type: gexp)
+A G-expression specifying a list environment variables for the Emacs
+server.  The value of this field, if set, will override all default
+environment variables, and users should likely specify a G-expression
+whose expansion includes elements from
+@code{default-environment-variables} (@pxref{Service De- and
+Constructors,,, shepherd,The GNU Shepherd Manual}).
+
+@item @code{load-custom?} (default: @code{#t}) (type: boolean)
+Whether to load customizations created with the Emacs customization
+interface.  When @code{inherit-directory?} is true, customizations made
+within this specific server affect other Emacsen, and vice versa.
+Otherwise, the server has its own separate set of customizations.
+
+@item @code{early-init-file} (default: @code{()}) (type: list-of-elisp-gexp-or-file-likes)
+A list of Elisp expressions, G-expressions, or file-like objects to
+serialize to the Emacs early init file, the @file{early-init.el} file in
+the server's Emacs configuration directory.  For file-like objects that
+represent actual files in the store (for example, those returned by
+@code{local-file} or @code{plain-file}), the contents of each file are
+serialized.
+
+When the @code{inherit-init-files?} for the server is true,
+configuration specified here is serialized to the early init file after
+any early init file configuration expressions specified by the Emacs
+Home service type.
+
+@item @code{init-file} (default: @code{()}) (type: list-of-elisp-gexp-or-file-likes)
+A list Elisp expressions, G-expressions, or file-like objects to
+serialize to the Emacs user initialization file, the @file{init.el} file
+in the server's Emacs configuration directory.  For file-like objects
+that represent actual files in the store (for example, those returned by
+@code{local-file} or @code{plain-file}), the contents of each file are
+serialized.
+
+When the @code{inherit-init-files?} for the server is true,
+configuration specified here is serialized to the Emacs user
+initialization file after the any user initialization file configuration
+expressions specified by the Emacs Home service type.
+
+@item @code{user-files} (default: @code{()}) (type: alist)
+An association list of filenames and file-like objects specifying files
+to create in the server's Emacs user directory.  For each entry, a file
+with the given filename will be created with the contents of the
+file-like object.  If a list of file-like objects is given for an entry,
+the new file will contain the combined text contents of all of the
+file-like objects in the list.  This field can be used to add
+configuration files for Emacs that should not be automatically loaded
+when Emacs is initialized.  Note that the Emacs user directory for the
+server may not be the same as the directory containing its Emacs user
+initialization file.
+
+When the @code{inherit-user-directory?} field is false, user files are
+created in a subdirectory of the user directory used by the Emacs Home
+service, which will be populated with copies of all of files specified
+in the @code{user-files} field of the @code{home-emacs-configuration}
+value for the service.
+
+@end table
+
+@end deftp
+
+@deftp {Data Type} home-emacs-extension
+Available @code{home-emacs-extension} fields are:
+
+@table @asis
+@item @code{packages} (default: @code{()}) (type: list-of-file-likes)
+A list of additional Emacs-related packages to install.
+
+@item @code{indent-forms} (default: @code{()}) (type: alist)
+An association list of symbols and indentation rules.  Each entry is of
+the form (@var{symbol} . @var{indent}), where @var{symbol} is a symbol
+and @var{indent} is an integer specifying the number of argument
+expressions for @var{symbol}.
+
+@item @code{early-init-file} (default: @code{()}) (type: list-of-elisp-gexp-or-file-likes)
+A list of Elisp expressions, G-expressions, or file-like objects to
+serialize to the Emacs early init file, the @file{early-init.el} file in
+the Emacs configuration directory.
+
+@item @code{init-file} (default: @code{()}) (type: list-of-elisp-gexp-or-file-likes)
+A list Elisp expressions, G-expressions, or file-like objects to
+serialize to the Emacs user initialization file, the @file{init.el} file
+in the Emacs configuration directory.
+
+@item @code{user-files} (default: @code{()}) (type: alist)
+An association list of filenames and file-like objects specifying files
+to create in the Emacs user directory.  Note that the Emacs user
+directory may not be the same as the directory containing the Emacs user
+initialization file.
+
+@item @code{servers} (default: @code{()}) (type: list-of-emacs-servers)
+A list of configurations for Emacs servers.  You cannot specify multiple
+@code{emacs-server} objects with equivalent @code{name} fields.
+
+@end table
+
+@end deftp
+
+@cindex Emacs packages, Home service
+@cindex Emacs packages, configuration
+@defvar home-emacs-packages-service-type
+This service type extends the @code{home-emacs-service-type} and
+provides useful abstractions to configure Emacs packages using Scheme.
+Its value is a @code{home-emacs-packages-configuration} object.
+
+Emacs packages are configured by specifying @code{emacs-package}
+objects, which each encapsulate configuration relating to a specific
+Emacs package.
+
+Here is a sample Guix Home configuration that utilizes the
+@code{home-emacs-packages-service-type} to configure some Emacs
+packages:
+
+@lisp
+(use-modules (gnu home)
+             (gnu services)
+             (guix gexp)
+             (gnu home services emacs)
+             (gnu packages emacs-xyz))
+
+(home-environment
+  (services
+   (list
+    (service
+     home-emacs-packages-service-type
+     (home-emacs-packages-configuration
+      (packages
+       (list
+        (emacs-package
+          (name 'emacs)
+          (extra-init-files `(("my-init.el"
+                               . ,(local-file "my-init.el"))))
+          (keys-global '(("C-x q" . bury-buffer)
+                         ("C-x C-b" . ibuffer)))
+          (options '((confirm-kill-emacs . y-or-n-p)
+                     (visible-bell . #t)
+                     (initial-scratch-message . #f)))
+          (extra-init
+           (elisp*
+            (setq-default indent-tabs-mode nil)
+            (tool-bar-mode -1)
+            (delete-selection-mode 1))))
+        (emacs-package
+          (name 'orderless)
+          (package emacs-orderless)
+          (options
+           '((completion-styles . (orderless basic))
+             (completion-category-overrides
+              . ((file (styles basic partial-completion))))))))))))))
+@end lisp
+
+@noindent
+The configuration above will install Emacs, configure some basic
+settings, and install and configure the @code{emacs-orderless} package.
+
+The value for the @code{packages} field of the
+@code{home-emacs-packages-configuration} is a list containing two
+@code{emacs-package} objects, each encapsulating configuration relating
+to a specific package.
+
+The first @code{emacs-package} object configures the Emacs package named
+@code{emacs}, that is, Emacs itself.  This is a convenient way to
+configure general settings related to no particular package, while
+leveraging the abstractions provided by the Emacs packages Home service.
+
+The second @code{emacs-package} object configures the package
+@code{emacs-orderless}.  Notice that the @code{package} field of the
+@code{emacs-package} object specifies a Guix package, which by default
+causes it to be installed.  In this way, we can conveniently manage the
+installation and configuration of Emacs packages in one place.
+
+The configuration given here is roughly equivalent to the first
+configuration example given for the @code{home-emacs-service-type}
+above.
+
+The @code{home-emacs-packages-service-type} can be extended with more
+@code{emacs-package} objects.  Here is an example:
+
+@lisp
+(define %gnus-init-file
+        (elisp-file "gnus.el"
+                    (elisp*
+                     (setopt gnus-select-method '(nnnil "")
+                             gnus-secondary-select-methods '((nnml ""))
+                             mail-sources
+                             '((imap :server "mail.example.net"
+                                     :user "user@@example.net"
+                                     :port 993
+                                     :stream tls))))))
+
+(home-environment
+ (services
+  (list
+   ...
+   (simple-service
+    'emacs-mail-service
+    home-emacs-packages-service-type
+    (list
+     (emacs-package
+      (name 'gnus)
+      (user-files
+       `(("gnus.el" . ,%gnus-init-file)
+         ("signature" . ,(local-file "signature"))))
+      (options
+       `((gnus-init-file . ,(elisp (locate-user-emacs-file "gnus.el")))
+         (mail-user-agent . gnus-user-agent)
+         (read-mail-command . gnus))))
+     (emacs-package
+      (name 'smtpmail)
+      (options '((smtpmail-servers-requiring-authorization
+                  . "mail\\.example\\.net")
+                 (smtpmail-smtp-server . "mail.example.net")
+                 (smtpmail-smtp-service . 587)
+                 (smtpmail-smtp-user . "user@@example.net"))))
+     (emacs-package
+      (name 'message)
+      (options `((message-send-mail-function . smtpmail-send-it)
+                 (message-signature-file
+                  . ,(elisp (locate-user-emacs-file "signature")))))))))))
+@end lisp
+
+@noindent
+The configuration above extends the
+@code{home-emacs-packages-service-type} to configure the Emacs Gnus
+Newsreader (@pxref{Top,,,Gnus,The Gnus Newsreader}), the @code{smtpmail}
+package, and the @code{message} packages so that Emacs can send and
+receive mail.  This configuration is equivalent to the example given
+above for extending the @code{home-emacs-service-type}.
+
+Note that the @code{options} field of @code{emacs-package} is an
+association list of option names and values that can accept an Elisp
+expression as a value.  Normally, values that do not represent
+self-quoting data types, like symbols or lists, are quoted when they are
+serialized to Elisp to prevent them from being evaluated by Emacs.
+However, when an Elisp expression is specified, the value is not quoted
+and is instead evaluated at Emacs initialization time.
+
+By using an Elisp expression in this way, the Emacs variable
+@code{message-signature-file} is set to the result of evaluating the
+function @code{locate-user-emacs-file} at run time.  If the s-expression
+was simply specified without using an Elisp expression, the option would
+be set to the list resulting from quoting the s-expression.
+
+@end defvar
+
+The record types for configuring the Emacs packages Home service are
+described in detail below.
+
+@deftp {Data Type} home-emacs-packages-configuration
+Available @code{home-emacs-packages-configuration} fields are:
+
+@table @asis
+@item @code{package-serializer} (type: emacs-package-serializer)
+The serializer to use for configuration specified by
+@code{emacs-package} objects.
+
+@item @code{packages} (default: @code{()}) (type: list-of-emacs-packages)
+A list of @code{emacs-package} objects containing configuration for
+Emacs packages.
+
+@end table
+
+@end deftp
+
+@deftp {Data Type} emacs-package-serializer
+Available @code{emacs-package-serializer} fields are:
+
+@table @asis
+@item @code{name} (type: symbol)
+A symbol identifying the serializer.
+
+@item @code{procedure} (type: procedure)
+A procedure that takes one argument, an @code{emacs-package} object, and
+that should return a list of @code{elisp} objects or G-expressions
+containing configuration to serialize to the Emacs user initialization
+file.
+
+@item @code{dependencies} (default: @code{()}) (type: list-of-packages)
+An list of additional packages to install whenever this serializer is
+used.
+
+@item @code{indent-forms} (default: @code{()}) (type: alist)
+An association list of symbols and indentation rules.  Each entry is of
+the form (@var{symbol} .  @var{indent}), where @var{symbol} is a symbol
+and @var{indent} is an integer.  Values have the same effect as the
+@code{indent-forms} field in the @code{home-emacs-configuration} record.
+Note that indentation rules specified here will subsequently affect all
+Emacs Lisp expressions serialized by the Emacs home service, not just
+configuration for the Emacs packages Home service.
+
+@item @code{extra-init} (default: @code{()}) (type: list-of-elisp-or-gexps)
+A list additional of Elisp expressions or G-expressions to serialize to
+the Emacs user initialization file before all serialized Emacs package
+configuration.  This is useful for code that must set up or configure
+the package serializer itself.
+
+@item @code{description} (default: @code{""}) (type: string)
+A brief description of the serializer.
+
+@end table
+
+@end deftp
+
+@deftp {Data Type} emacs-package
+Available @code{emacs-package} fields are:
+
+@table @asis
+@item @code{name} (type: symbol)
+The symbol naming the Emacs package or library, as would be used with
+Emacs @code{require}.
+
+@item @code{package} (default: @code{()}) (type: package-or-null)
+A Guix package providing the Emacs package specified by @code{name}.  If
+the package is built into Emacs, or if there is no associated Guix
+package, this field should be set to the empty list (the default).
+
+@item @code{extra-packages} (default: @code{()}) (type: list-of-packages)
+A list of packages that provide additional functionality used by this
+package, but which are not installed automatically by the Guix package
+manager as propagated inputs of @code{package}.
+
+@item @code{install?} (default: @code{#t}) (type: boolean)
+Whether to install @code{package} and @code{extra-packages}.
+
+@item @code{extra-init-files} (default: @code{()}) (type: alist)
+An association list of filenames and file-like objects containing Emacs
+Lisp to load when Emacs is initialized.  For each entry, a file with the
+text contents of the file-like object, or the combined text contents of
+all of the file-like objects in a list if a list is specified, will be
+created with the given filename in the appropriate Emacs configuration
+directory (the directory where the @file{early-init.el} and
+@file{init.el} files are located).  These files will then be loaded when
+Emacs is initialized.
+
+This can be used as an escape hatch, similarly to the @code{extra-init}
+field.  It is useful when large amounts of code must be loaded in order
+to configure a package.
+
+Note that it is an error to specify files with the filenames
+@samp{init.el} and @samp{early-init.el}, because these files are already
+generated by the Emacs home service.
+
+Also note that it is up to the package serializer whether and when to
+load these files, although the @code{home-emacs-service-type} ensures
+that the files will be present in the relevant configuration
+directories.  @code{%emacs-simple-package-serializer} and
+@code{%emacs-use-package-serializer} both load the files at
+initialization time, before any expressions given in the
+@code{extra-init} field, regardless of the value of the
+@code{load-after-packages} field.
+
+@item @code{user-files} (default: @code{()}) (type: alist)
+An association list of filenames and file-like objects specifying files
+to create in the Emacs user directory.  For each entry, a file with the
+given filename will be created in the Emacs user directory with the
+contents of the file-like object.  If a list of file-like objects is
+given for an entry, the new file will contain the combined text contents
+of all of the file-like objects in the list.  This field should be used
+to add per-package files to the Emacs user directory.
+
+@item @code{load-force?} (default: @code{#f}) (type: boolean)
+Whether to force loading of this package immediately when Emacs is
+initialized, rather than deferring loading, for example, until an
+autoloaded function is invoked.  This is similar in effect to the
+keyword @code{:demand} from @code{use-package} and to the inverse of the
+keyword @code{:defer}.  The difference is that when this field is false,
+package loading should always be deferred; @code{use-package} normally
+does not defer loading when it does not set up autoloads, because it
+doesn't know that Guix handles autoloads on its own.
+
+@item @code{load-predicates} (default: @code{()}) (type: list-of-elisp-or-gexps)
+A list predicate expressions to evaluate when Emacs is initialized to
+determine whether to evaluate the configuration for this package.  When
+this list is not empty, @emph{all} other configuration for this package
+should be effectively surrounded in the Emacs user initialization file
+by a block of the form: @code{(when @var{load-predicates} @dots{})}.
+This is the supercharged Guix version of the @code{use-package}
+@code{:if} keyword!
+
+If multiple load predicates are specified, the behavior is determined by
+the package configuration serializer.  Both
+@code{%emacs-simple-package-serializer} and
+@code{%emacs-use-package-serializer} compose load predicates using
+@code{and}, so that all load predicates in the list must be satisfied in
+order for the package configuration to be evaluated.
+
+@item @code{load-after-packages} (default: @code{()}) (type: list-of-symbols)
+A list of symbols for Emacs packages that must be loaded before this
+package is loaded.  Only after all of the packages in the list have been
+loaded by Emacs should configuration for this package be evaluated.  This
+is similar to a simplified version of the @code{:after} keyword from
+@code{use-package}.
+
+@item @code{load-paths} (default: @code{()}) (type: list-of-string-or-file-likes)
+A list of additional load paths to add to the Emacs @code{load-paths}
+variable.  Load paths can be specified either as strings or as file-like
+objects, in which case a path to the respective store item is
+substituted.
+
+@item @code{autoloads} (default: @code{()}) (type: list-of-symbols)
+A list of Emacs functions from the package to autoload.  This can be
+useful, for example, when defining custom commands in the Emacs user
+initialization file that use functions which are not autoloaded by
+default.
+
+@item @code{autoloads-interactive} (default: @code{()}) (type: list-of-symbols)
+A list of additional Emacs interactive commands from the package to
+autoload, so that they can be invoked interactively before the package
+is loaded.
+
+@item @code{keys-global} (default: @code{()}) (type: alist)
+An association list of key sequences (as strings or vectors) and Emacs
+commands to bind in the global keymap.
+
+@item @code{keys-global-keymaps} (default: @code{()}) (type: alist)
+An association list of key sequences and Emacs keymap variables to bind
+to them in the global keymap.  The keymap variables should be symbols
+that define keymaps in the package; they can be effectively autoloaded
+using this assumption.
+
+@item @code{keys-override} (default: @code{()}) (type: alist)
+An association list of key sequences and symbols naming Emacs commands
+to bind in the global override map.  These key bindings have a higher
+precedence than local and global keybindings.
+
+@item @code{keys-local} (default: @code{()}) (type: list-of-emacs-keymaps)
+A list of key binding configurations for specific keymaps, each
+contained in an @code{emacs-keymap} object.
+
+@item @code{options} (default: @code{()}) (type: alist)
+An association list of user options and values for this package.  Options
+should be symbols naming Emacs variables, and values can be any object
+that can be serialized to Elisp.  For values, primitive Scheme data
+types are implicitly quoted, including lists and symbols.  To instead
+set an option to the value of an expression to be evaluated at Emacs
+initialization time, either use an Elisp expression (e.g., specified
+with the @code{elisp} form) or a G-expression for a value.
+
+@item @code{faces} (default: @code{()}) (type: alist)
+An association list of face symbols and face specs.  @xref{Defining
+Faces,,,elisp,The Emacs Lisp Manual} for the format of face specs.
+
+@item @code{hooks} (default: @code{()}) (type: alist)
+An association list of hooks and functions to add to them.  Each entry
+is a pair of symbols.  Hook symbols in Emacs should end in @samp{-hook},
+but the @code{%emacs-simple-package-serializer} and
+@code{%emacs-use-package-serializer} serializers effectively add this
+suffix when necessary.
+
+@item @code{auto-modes} (default: @code{()}) (type: alist)
+An association list of filename patterns as regular expression strings
+and Emacs mode functions to call when visiting files with filenames that
+match the patterns.  @xref{Auto Major Mode,,,elisp,The Emacs Lisp
+Manual} for details.
+
+@item @code{magic-modes} (default: @code{()}) (type: alist)
+An association list regular expression strings and Emacs mode functions
+to call when visiting files that begin with matching text.  @xref{Auto
+Major Mode,,,elisp,The Emacs Lisp Manual} for details.
+
+@item @code{extra-after-load} (default: @code{()}) (type: list-of-elisp-or-gexps)
+A list of Elisp expressions or G-expressions to evaluate after the
+package is loaded, as with the Emacs @code{eval-after-load} function.
+Elisp expressions can be specified using the @code{elisp} and
+@code{elisp*} forms.
+
+@item @code{extra-init} (default: @code{()}) (type: list-of-elisp-or-gexps)
+A list of Elisp expressions or G-expressions to evaluate immediately
+when Emacs is initialized, even if loading is deferred due to the
+@code{load-force?} field.  Note that the @code{load-predicates} field
+should still determine whether these expressions are evaluated, and they
+will only be evaluated after all packages specified in the
+@code{load-after-packages} field have been loaded.
+
+@item @code{extra-keywords} (default: @code{()}) (type: alist)
+An association list of keys and lists of extra Elisp expressions or
+G-expressions.  Keys can potentially be any keyword or symbol object;
+keywords are automatically serialized to their Emacs Lisp equivalent
+(e.g., @code{#:keyword} is serialized as @code{:keyword}).  The meanings
+of entries is specific to each package serializer, and any key may be
+ignored by a package serializer.  This field is currently ignored by the
+@code{%emacs-simple-package-serializer}.  Entries in this list matching
+@code{use-package} keywords will be spliced by the
+@code{%emacs-use-package-serializer} into the @code{use-package} body,
+after all other forms.
+
+@end table
+
+@end deftp
+
+@deftp {Data Type} emacs-keymap
+Available @code{emacs-keymap} fields are:
+
+@table @asis
+@item @code{name} (default: @code{global-map}) (type: symbol)
+The symbol of the Emacs keymap in which to bind keys.
+
+@item @code{package-name} (default: @code{#f}) (type: symbol-or-false)
+The symbol naming the Emacs package providing the keymap, as would be
+used with Emacs @code{require}.  If this field is false (the default),
+then the package for which the keymap is being configured should define
+the keymap or the keymap should otherwise be defined by the time the
+configuration for the package is evaluated.  Note that when the
+@code{prefix?} field is true, this implies that the keymap will be
+defined and this field has no effect.
+
+@item @code{prefix?} (default: @code{#f}) (type: boolean)
+Whether to create a new keymap and assign it to a prefix command named
+@code{name}.  Specifically, the new keymap is assigned to the function
+definition of the symbol @code{name}.  This is useful when the keymap
+does not already exist, but the user would like to bind it to a prefix
+key using the @code{keys-global} field in an @code{emacs-package}
+configuration object or even using the @code{keys} field in another
+@code{emacs-keymap} object.  Because a prefix command is a valid
+function, no special handling is needed to use it in keybinding
+definitions where commands are expected.  Another benefit of prefix
+commands comes when using keybinding introspection features such as
+@code{which-key-mode}: @code{name} will be displayed as a prefix key
+description when it is bound to a prefix command declared in this way,
+whereas ad-hoc prefix key bindings like @kbd{C-c d d} where @kbd{C-c d}
+is not explicitly defined will not have explicitly meaningful
+descriptions.
+
+Note that when this field true, the @code{package-name} field has no
+effect.
+
+@item @code{repeat?} (default: @code{#f}) (type: boolean)
+Whether to make this keymap a repeat map (@pxref{Repeating,,, emacs,The
+GNU Emacs Manual}).  Repeat maps are created by setting the
+@code{repeat-map} symbol property for each key definition in @code{keys}
+to the @code{name} of this keymap.  Use the @code{repeat-exit} field to
+override this setting for specific bindings.
+
+@item @code{repeat-exit} (default: @code{()}) (type: list-of-symbols)
+A list of commands that exit the repeat map.  When @code{repeat?} is
+true, these commands do not get the @code{repeat-map} property.  The
+meaning of this field is similar to that of the @code{:exit} keyword
+used by the @code{defvar-keymap} function in Emacs.  This field has no
+effect when @code{repeat?} is false.
+
+@item @code{repeat-enter} (default: @code{()}) (type: list-of-symbols)
+A list of additional commands that enter the repeat map.  When
+@code{repeat?} is true, these commands get the @code{repeat-map}
+property, even when they are not bound in the keymap.  This is only
+useful when a command is not bound in @code{name}, but the repeat map
+should be accessible after that command is invoked (e.g., with
+@kbd{M-x}).  The meaning of this field is similar to that of the
+@code{:enter} keyword used by the @code{defvar-keymap} function in
+Emacs.  This field has no effect when @code{repeat?} is false.
+
+@item @code{disabled-commands} (default: @code{()}) (type: alist)
+An association list of command symbols and whether to disable them.  When
+a disabled command is interactively invoked, Emacs asks for confirmation
+from the user (@pxref{Disabling,,, emacs,The GNU Emacs Manual}).  The
+values of this alist should be booleans, which will be stored as the
+value of the @code{disabled} property of each respective command symbol.
+Thus, to disable the @code{transpose-chars} command and enable the
+@code{erase-buffer} command, you can use:
+
+@lisp
+'((transpose-chars .  #t)
+  (erase-buffer .  #f))
+@end lisp
+
+@item @code{keys} (default: @code{()}) (type: alist)
+An association list of key sequences and binding definitions.  Key
+sequences are Emacs-specific string or vector representations of
+sequences of keystrokes or events.  Strings should be valid arguments to
+the Emacs function @code{kbd}, and they are preferred over the low-level
+vector representations (@pxref{Keymaps,,, elisp,The Emacs Lisp Manual}).
+Binding definitions should be Emacs command symbols.  As a special case,
+when a binding definition is the boolean false, the key is unset in the
+keymap.
+
+@end table
+
+@end deftp
+
+
+@cindex Emacs package serializers, for Emacs packages Home service
+As we have seen, we can customize how configuration for Emacs packages
+is serialized to the Emacs user initialization file by using the
+@code{package-serializer} field of the
+@code{home-emacs-packages-configuration} record type.
+
+There are two predefined package serializers:
+@code{%emacs-simple-package-serializer} and
+@code{%emacs-use-package-serializer}.
+
+@defvar %emacs-simple-package-serializer
+An Emacs package configuration serializer that configures Emacs using
+minimal, built-in Emacs mechanisms, instead of complex macros such as
+@code{use-package}.
+@end defvar
+
+@defvar %emacs-use-package-serializer
+An Emacs package configuration serializer that configures Emacs with the
+@code{use-package} macro.
+@end defvar
+
+@cindex Emacs package serializers, defining for Emacs packages Home service
+We can also create custom package serializers by defining our own
+@code{emacs-package-serializer} objects.  The two mandatory fields are
+the @code{name} and the @code{procedure} fields.  @code{name} is an
+arbitrary symbol (currently unused), and @code{procedure} is a procedure
+that takes one argument, an @code{emacs-package} object to be
+serialized, and should return a list of Elisp expressions or
+G-expressions to serialize to the Emacs initialization file.
+
+When defining a new package serializer, it is advisable to refer to the
+documentation for the @code{emacs-package} record type, which lays out
+some implementation guidelines that package serializers should follow
+for each field.  It is up to you to implement your serializer in a way
+that is consistent with those guidelines, or not.
+
+Here is an example of a hypothetical
+@code{%emacs-null-package-serializer} that only serializes a simple
+comment naming each package:
+
+@lisp
+(define (emacs-package->null-elisp config)
+  "Return from 'emacs-package' CONFIG a list of Elisp expressions that
+configures Emacs by serializing only comments."
+  (match-record config <emacs-package>
+                (name)
+    (let ((comment-string (string-append ";;; "
+                                         (symbol->string name)
+                                         "\n")))
+      (list (elisp (unelisp-comment comment-string))))))
+
+(define %emacs-null-package-serializer
+  (emacs-package-serializer
+   (name 'emacs-null-package)
+   (procedure emacs-package->null-elisp)
+   (description "An Emacs package serializer that doesn't do anything.")))
+@end lisp
+
+@cindex importing configuration, for Emacs Home services
+You may have existing Emacs initialization files, but translating them
+into Scheme configuration records can be tedious.  The @command{guix
+home import} command can be used to generate Emacs Home service
+configurations from existing Emacs configuration files (@pxref{Declaring
+the Home Environment}).  Additionally, the @code{(gnu home services
+emacs)} module provides two utility functions to aid in this process:
+@code{input->home-emacs-packages-configuration} and
+@code{input->home-emacs-packages-configuration-sexp}.
+
+The following example returns an s-expression that declares a
+@code{home-emacs-packages-configuration} record corresponding to the
+given Emacs initialization file:
+
+@lisp
+(call-with-input-file "/home/user/.config/emacs/init.el"
+  (lambda (port)
+    (input->home-emacs-packages-configuration-sexp port)))
+@end lisp
+
+@deffn {Procedure} input->home-emacs-packages-configuration port
+Return a @code{home-emacs-packages-configuration} record from Elisp code
+read from @var{port}.
+@end deffn
+
+@deffn {Procedure} input->home-emacs-packages-configuration-sexp port
+Return a s-expression in Scheme that declares a
+@code{home-emacs-packages-configuration} record from Elisp code read
+from @var{port}.
+@end deffn
 
 @node Guix Home Services
 @subsection Guix Home Services
diff --git a/gnu/home/services/emacs.scm b/gnu/home/services/emacs.scm
new file mode 100644
index 0000000000..585a4b4d6f
--- /dev/null
+++ b/gnu/home/services/emacs.scm
@@ -0,0 +1,3101 @@
+;;; GNU Guix --- Functional package management for GNU
+
+;;; Copyright © 2023 ( <[email-masked]>
+;;; Copyright © 2023 David Wilson <[email-masked]>
+;;; Copyright © 2023-2025 Kierin Bell <[email-masked]>
+;;;
+;;; This file is part of GNU Guix.
+;;;
+;;; GNU Guix is free software; you can redistribute it and/or modify it
+;;; under the terms of the GNU General Public License as published by
+;;; the Free Software Foundation; either version 3 of the License, or (at
+;;; your option) any later version.
+;;;
+;;; GNU Guix is distributed in the hope that it will be useful, but
+;;; WITHOUT ANY WARRANTY; without even the implied warranty of
+;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;;; GNU General Public License for more details.
+;;;
+;;; You should have received a copy of the GNU General Public License
+;;; along with GNU Guix.  If not, see <http://www.gnu.org/licenses/>.
+
+(define-module (gnu home services emacs)
+  #:use-module (gnu home services)
+  #:use-module (gnu home services shepherd)
+  #:autoload   (gnu packages emacs) (emacs emacs-minimal)
+  #:autoload   (gnu packages emacs-xyz) (emacs-use-package)
+  #:use-module (gnu services configuration)
+  #:use-module (guix gexp)
+  #:use-module (guix records)
+  #:use-module (guix packages)
+  #:use-module (guix utils)
+  #:use-module (guix modules)
+  #:use-module (guix read-print)
+  #:use-module (guix formatters elisp)
+  #:use-module (guix store)
+  #:use-module (guix monads)
+  #:use-module (guix i18n)
+  #:use-module ((guix diagnostics)
+                #:select (formatted-message))
+  #:use-module (srfi srfi-1)
+  #:use-module (srfi srfi-26)
+  #:use-module (srfi srfi-35)
+  #:use-module (ice-9 match)
+  #:use-module (ice-9 receive)
+  #:use-module (ice-9 binary-ports)
+  #:use-module (rnrs bytevectors)
+  #:use-module (ice-9 control)
+  #:use-module (ice-9 regex)
+  #:use-module (language elisp parser)
+  #:export (elisp?
+            elisp->sexp
+            sexp->elisp
+            elisp
+            elisp*
+
+            elisp-file
+            elisp-file*
+            composite-elisp-file
+
+            emacs-server
+            emacs-server?
+            emacs-server-name
+            emacs-server-inherit-user-directory?
+            emacs-server-inherit-init-files?
+            emacs-server-auto-start?
+            emacs-server-debug?
+            emacs-server-shepherd-requirements
+            emacs-server-environment-variables
+            emacs-server-load-custom?
+            emacs-server-early-init-file
+            emacs-server-init-file
+            emacs-server-user-files
+
+            home-emacs-configuration
+            home-emacs-configuration?
+            home-emacs-configuration-emacs
+            home-emacs-configuration-packages
+            home-emacs-configuration-user-emacs-directory
+            home-emacs-configuration-native-compile?
+            home-emacs-configuration-indent-forms
+            home-emacs-configuration-load-custom?
+            home-emacs-configuration-early-init-file
+            home-emacs-configuration-init-file
+            home-emacs-configuration-user-files
+            home-emacs-configuration-servers
+
+            home-emacs-extension
+            home-emacs-extension?
+            home-emacs-extension-packages
+            home-emacs-extension-indent-forms
+            home-emacs-extension-early-init-file
+            home-emacs-extension-init-file
+            home-emacs-extension-user-files
+            home-emacs-extension-servers
+
+            emacs-server->provision
+
+            home-emacs-service-type
+
+            emacs-keymap
+            emacs-keymap?
+            emacs-keymap-name
+            emacs-keymap-package-name
+            emacs-keymap-prefix?
+            emacs-keymap-repeat?
+            emacs-keymap-repeat-exit
+            emacs-keymap-repeat-enter
+            emacs-keymap-keys
+
+            emacs-package
+            emacs-package?
+            emacs-package-name
+            emacs-package-package
+            emacs-package-extra-packages
+            emacs-package-install?
+            emacs-package-extra-init-files
+            emacs-package-user-files
+            emacs-package-load-force?
+            emacs-package-load-predicates
+            emacs-package-load-after-packages
+            emacs-package-load-paths
+            emacs-package-autoloads
+            emacs-package-autoloads-interactive
+            emacs-package-keys-global
+            emacs-package-keys-global-keymaps
+            emacs-package-keys-override
+            emacs-package-keys-local
+            emacs-package-options
+            emacs-package-faces
+            emacs-package-hooks
+            emacs-package-auto-modes
+            emacs-package-magic-modes
+            emacs-package-extra-after-load
+            emacs-package-extra-init
+            emacs-package-extra-keywords
+
+            emacs-package-serializer
+            emacs-package-serializer?
+            emacs-package-serializer-name
+            emacs-package-serializer-procedure
+            emacs-package-serializer-dependencies
+            emacs-package-serializer-indent-forms
+            emacs-package-serializer-extra-init
+            emacs-package-serializer-description
+
+            %emacs-extra-init-files-path
+            %emacs-extra-init-files-path-variable
+            emacs-package-extra-init-file->sexp
+
+            emacs-package->simple-elisp
+            %emacs-simple-package-serializer
+            emacs-package->use-package-elisp
+            %emacs-use-package-serializer
+
+            home-emacs-packages-configuration
+            home-emacs-packages-configuration?
+            home-emacs-packages-configuration-package-serializer
+            home-emacs-packages-configuration-packages
+
+            emacs-packages->elisp
+
+            home-emacs-packages-service-type
+
+            input->home-emacs-packages-configuration
+            input->home-emacs-packages-configuration-sexp))
+
+;;; Commentary:
+;;;
+;;; Services for the GNU Emacs extensible text editor.
+;;;
+;;; Code:
+
+
+;;;
+;;; Elisp expression objects.
+;;;
+
+(define-record-type* <elisp> %elisp
+  make-elisp
+  elisp?
+  this-elisp
+  (sexp elisp-sexp))
+
+(define (dotted-list?* obj)
+  (and (pair? obj)
+       (dotted-list? obj)))
+
+(define list->dotted-list
+  (match-lambda
+    ((? list? lst)
+     (match (last-pair lst)
+       (((? pair?) . ())
+        ;; Prevent, e.g., '((quote a) (quote b)) -> '((quote a) quote b).
+        lst)
+       (_
+        (apply cons* lst))))
+    (x
+     x)))
+
+(define (fold-right/elisp fhere fup fcons seed exp)
+  "Recurse into subexpressions and 'elisp' objects in EXP, applying FHERE, FUP
+and FCONS.  FHERE transforms atoms, FUP transforms accumulators after
+traversing lists, and FCONS joins atoms or lists with accumulators while
+traversing lists.  FHERE and FCONS each take two arguments: an element and an
+accumulator.  For FUP, the second argument is the accumulator, and the first
+argument is either the empty list or false if its second argument was derived
+from a dotted list.  The accumulator starts as SEED."
+  (define (reverse* lst)
+    (let loop ((lst lst)
+               (acc '()))
+      (match lst
+        ((? null?)
+         acc)
+        ((not (? pair?))
+         ;; Convert dotted lists into proper lists.
+         (cons lst acc))
+        ((head . tail)
+         (loop tail (cons head acc))))))
+
+  (match exp
+    ((? elisp?)
+     (fold-right/elisp fhere fup fcons seed (elisp-sexp exp)))
+    ((or (not (? pair?))
+         (? null?))
+     ;; The empty list should be passed to FHERE along with atoms.
+     (fhere exp seed))
+    (_
+     (let loop ((exp (reverse* exp))
+                (acc seed)
+                (dotted? (dotted-list?* exp)))
+       (match exp
+         ((? null?)
+          ;; XXX: FUP must handle any transformation of ACC back into a dotted
+          ;; list, since FHERE could have transformed the last element of ACC
+          ;; into a list, in which case we can't just use 'list->dotted-list'
+          ;; to get a dotted list back.
+          (fup (if dotted? #f exp) acc))
+         ((head . tail)
+          (loop tail
+                (fcons (fold-right/elisp fhere fup fcons seed head)
+                       acc)
+                dotted?)))))))
+
+(define (elisp->sexp exp)
+  "Return an s-expression containing the contents of Elisp expression EXP."
+  (fold-right/elisp (lambda (t s) t)
+                    (lambda (t s)
+                      (if (not t) (list->dotted-list s) s))
+                    cons
+                    '()
+                    exp))
+
+(define (elisp->lowerable-sexp exp)
+  "Return an s-expression from EXP that is lowerable within a G-expression,
+that is, strip '<vertical-space>', '<page-break>', and '<comment>' objects."
+  (let ((result (fold-right/elisp (lambda (t s) t)
+                                  (lambda (t s)
+                                    (if (not t) (list->dotted-list s) s))
+                                  (lambda (t s) (if (blank? t) s (cons t s)))
+                                  '()
+                                  exp)))
+    ;; XXX: What to do when an Elisp expression *is* a <blank>?
+    (if (blank? result) '() result)))
+
+(define-gexp-compiler (elisp-compiler (elisp <elisp>) system target)
+  ;; "Compile" an 'elisp' object by stripping '<vertical-space>',
+  ;; '<page-break>', and '<comment>' objects, so that it can be 'ungexp'd
+  ;; within a G-expression.
+  (with-monad %store-monad
+    (return (elisp->lowerable-sexp elisp))))
+
+(define (sexp->elisp sexp)
+  "Return an Elisp expression object containing SEXP."
+  (%elisp (sexp sexp)))
+
+(define-syntax elisp
+  ;; Create an '<elisp>' object from EXP, including any substitutions made
+  ;; with 'unelisp', 'unelisp-splicing', 'unelisp-comment', 'unelisp-newline',
+  ;; or 'unelisp-page-break'.  Modified from the 'gexp' macro in (guix gexp).
+  (lambda (s)
+    (define (substitute-unelisp e)
+      (syntax-case e (unelisp
+                      unelisp-splicing
+                      unelisp-comment
+                      unelisp-newline
+                      unelisp-page-break)
+        ((unelisp exp)
+         #'exp)
+        (((unelisp-splicing exp) rest ...)
+         #`(append exp #,(substitute-unelisp #'(rest ...))))
+        ((unelisp-comment str)
+         #'(comment str))
+        ((unelisp-newline)
+         #'(vertical-space 0))
+        ((unelisp-page-break)
+         #'(page-break))
+        ((exp0 . exp)
+         #`(cons #,(substitute-unelisp #'exp0)
+                 #,(substitute-unelisp #'exp)))
+        (x #''x)))
+
+    (syntax-case s ()
+      ((_ exp)
+       (let ((sexp* (substitute-unelisp #'exp)))
+         #`(%elisp (sexp #,sexp*)))))))
+
+(define-syntax elisp*
+  ;; Return a list of <elisp> objects which contains each EXP.
+  (lambda (s)
+    (syntax-case s ()
+      ((_ exp ...)
+       (with-syntax (((elts ...)
+                      (map
+                       (lambda (e)
+                         #`(elisp #,e))
+                       #'(exp ...))))
+         #'(list elts ...))))))
+
+
+;;;
+;;; Elisp files.
+;;;
+
+(define %printer-module-closure (source-module-closure
+                                 '((guix formatters elisp)
+                                   (guix read-print))))
+
+(define (constant? obj)
+  "Return whether OBJ is self-quoting."
+  (or (boolean? obj)
+      (char? obj)
+      (string? obj)
+      (keyword? obj)
+      (number? obj)
+      (array? obj)))
+
+(define* (elisp->printer exp #:key (special-forms '()))
+  "Return a G-expression containing a lambda that prints the Elisp
+expression (<elisp> objects or s-expressions) or G-expression EXP to the port
+specified in its argument.  See 'elisp-file' for a description of
+SPECIAL-FORMS."
+  (define (object->pp-quoted exp)
+    (match exp
+      ((? vertical-space?)
+       `((@ (guix read-print) vertical-space)
+         ,(vertical-space-height exp)))
+      ((? page-break?)
+       '((@ (guix read-print) page-break)))
+      ((? comment?)
+       `((@ (guix read-print) comment)
+         ,(comment->string exp)))
+      ((? constant?)
+       exp)
+      (_
+       (list 'quote exp))))
+
+  (define (elisp->pp-arg exp)
+    ;; Doing some of this on the derivation side with a macro similar to
+    ;; 'quasiquote' might be cleaner, at the expense of an extra tree
+    ;; traversal.
+    (fold-right/elisp (lambda (t s)
+                        (object->pp-quoted t))
+                      (lambda (t s)
+                        (if (not t)
+                            ;; Transform S back into a dotted list, but only
+                            ;; after Scheme 'quote' forms have been evaluated.
+                            ;; Also, never allow <comment>, <vertical-space>,
+                            ;; or <page-break> record constructors to
+                            ;; terminate a dotted list ('pretty-print-elisp'
+                            ;; shouldn't print them anyway).
+                            (match (last-pair s)
+                              ((`((@ (guix read-print) ,(or 'vertical-space
+                                                            'page-break
+                                                            'comment)) . ,_)
+                                . ())
+                               `(list ,@s))
+                              (_ `(apply cons* (list ,@s))))
+                            `(list ,@s)))
+                      cons
+                      '()
+                      exp))
+
+  (with-imported-modules %printer-module-closure
+    #~(lambda (port)
+        #$@(match exp
+             ((or (? gexp? exp)
+                  (and (? file-like?) (not (? elisp?)) exp))
+              ;; 'ungexp' G-expressions or anything that has a G-expression
+              ;; compiler other than Elisp objects.
+              (list
+               #~((@ (guix formatters elisp) pretty-print-elisp)
+                  port
+                  (quote #$exp)
+                  #:special-forms '#$special-forms)
+               #~(display "\n" port)))
+             (exp
+              ;; S-expressions can be treated like 'elisp' objects, and we can
+              ;; use s-exps internally to avoid the overhead of converting
+              ;; 'elisp' objects back into s-exps.
+              (append
+               (list
+                #~((@ (guix formatters elisp) pretty-print-elisp)
+                   port
+                   #$(elisp->pp-arg exp)
+                   #:format-comment
+                   (@ (guix read-print) canonicalize-comment)
+                   #:special-forms '#$special-forms))
+               (if (comment? exp)
+                   '()
+                   (list
+                    #~(display "\n" port)))))))))
+
+(define* (elisp->file-builder exps #:key (special-forms '()))
+  "Return a G-expression that builds a file containing the Elisp
+expressions (<elisp> objects or s-expressions) or G-expressions in list EXPS.
+See 'elisp-file' for a description of SPECIAL-FORMS."
+  #~(begin
+      (call-with-output-file #$output:out
+        (lambda (port)
+          ;; XXX: Set the encoding to allow strings to be displayed without
+          ;; Unicode characters being mangled in store items.
+          (set-port-encoding! port "UTF-8")
+          #$@(map
+              (lambda (exp)
+                #~(apply
+                   #$(elisp->printer exp #:special-forms special-forms)
+                   (list port)))
+              exps)))))
+
+(define* (elisp-file name exps #:key (special-forms '()))
+  "Return an object representing the store file NAME, an Emacs Lisp file that
+contains EXPS, a list of Elisp expression objects or G-expressions.
+
+Custom indentation rules can be specified with SPECIAL-FORMS, an association
+list where each entry is of the form (SYMBOL . INDENT).  When SYMBOL occurs at
+the beginning of a list in an expression in EXPS, the first INDENT expressions
+after SYMBOL are indented as arguments and the remainder are indented as body
+expressions, as if INDENT was the value of the 'lisp-indent-function' symbol
+property for SYMBOL in Emacs.  As in Emacs, argument expressions, if they
+cannot be pretty-printed on the same line as SYMBOL, are indented 4 columns
+beyond the base indentation of the enclosing list, and body expressions are
+indented 2 columns beyond the base indentation.
+
+This is the declarative counterpart of 'elisp-file*'."
+  (define builder
+    (elisp->file-builder exps #:special-forms special-forms))
+
+  (computed-file name
+                 builder
+                 #:local-build? #t
+                 #:options '(#:substitutable? #f)))
+
+(define* (elisp-file* name exps #:key (special-forms '()))
+  "Return as a monadic value a derivation that builds an Elisp file named NAME
+containing the expressions in EXPS, a list of Elisp expression objects or
+G-expressions.
+
+This is the monadic counterpart of 'elisp-file', which see for a description
+of SPECIAL-FORMS."
+  (define builder
+    (elisp->file-builder exps #:special-forms special-forms))
+
+  (gexp->derivation name builder
+                    #:local-build? #t
+                    #:substitutable? #f))
+
+(define* (elisp-or-file-like->file-builder exps #:key (special-forms '()))
+  "Return a G-expression that builds a file containing the Elisp
+expressions (<elisp> objects or s-expressions), G-expressions, or file-likes
+in list EXPS.  The contents of file-like objects are spliced into the output
+file.  See 'elisp-file' for a description of SPECIAL-FORMS."
+  (with-imported-modules (source-module-closure
+                          '((ice-9 binary-ports)
+                            (rnrs bytevectors)))
+    #~(begin
+        (call-with-output-file #$output:out
+          (lambda (port)
+            ;; XXX: Set the encoding to allow strings to be displayed without
+            ;; Unicode characters being mangled in store items (for
+            ;; 'elisp->printer').
+            (set-port-encoding! port "UTF-8")
+            #$@(map
+                (match-lambda
+                  ;; XXX: Only try to splice contents of file-likes that are
+                  ;; actually files.
+                  ((or (? local-file? exp)
+                       (? plain-file? exp)
+                       (? computed-file? exp)
+                       (? program-file? exp))
+                   ;; XXX: Binary I/O ensures that Unicode is not mangled.
+                   ;; 'get-bytevector-all' returns <#eof> when port is empty.
+                   #~(let ((bv (call-with-input-file #$exp
+                                 (@ (ice-9 binary-ports)
+                                    get-bytevector-all))))
+                       ((@ (ice-9 binary-ports) put-bytevector) port
+                        (if (eof-object? bv)
+                            ((@ (rnrs bytevectors) make-bytevector) 0)
+                            bv))))
+                  (exp
+                   #~(apply
+                      #$(elisp->printer exp #:special-forms special-forms)
+                      (list port))))
+                exps))))))
+
+(define* (composite-elisp-file name exps #:key (special-forms '()))
+  "Return an object representing the store file NAME, an Emacs Lisp file that
+contains EXPS, a list of Elisp expression objects, G-expressions, or file-like
+objects.  The contents of file-like objects are spliced into the output file.
+See 'elisp-file' for a description of SPECIAL-FORMS."
+  (define builder
+    (elisp-or-file-like->file-builder exps #:special-forms special-forms))
+
+  (computed-file name
+                 builder
+                 #:local-build? #t
+                 #:options '(#:substitutable? #f)))
+
+
+;;;
+;;; Helper functions
+;;;
+
+(define-syntax alist-sanitizer
+  ;; Construct a lambda expression that matches each KEY-PAT and VALUE-PAT
+  ;; pair against each entry of its argument, an alist.  If no pair matches,
+  ;; or if its argument is not an alist, the lambda signals an error
+  ;; displaying FIELD-NAME and the value of its argument.  Otherwise, the
+  ;; return value of the lambda is its argument.
+  (lambda (s)
+    (syntax-case s ()
+      ((_ field-name (key-pat . value-pat) ...)
+       (with-syntax (((clauses ...)
+                      (map
+                       (lambda (spec)
+                         (syntax-case spec ()
+                           ((key-pat . value-pat)
+                            ;; Note that entries of the form (A B) are
+                            ;; equivalent to (A . (B))---i.e., the tail is
+                            ;; really a list, not an atom.  However, (A B)
+                            ;; where B is an Elisp expression is converted
+                            ;; into (A . B).
+                            #'(((and key-pat key)
+                                . (and value-pat value))
+                               (cons key value)))))
+                       #'((key-pat . value-pat) ...))))
+         #'(lambda (val)
+             (map
+              (lambda (expr)
+                (match expr
+                  clauses ...
+                  (_ (configuration-field-error #f 'field-name val))))
+              val))))
+      ((_ field-name '(key-pat . value-pat) ...)
+       #'(alist-sanitizer field-name (key-pat . value-pat) ...))
+      ((_ field-name)
+       #'(alist-sanitizer field-name (_ . _))))))
+
+(define (ensure-list obj)
+  "Return OBJ as a list."
+  (if (list? obj) obj (list obj)))
+
+(define (merge-alists lst1 . rest)
+  "Combine association list LST1 and the association lists in REST, merging
+the values for any duplicate keys into a single list value.  Key comparison is
+done with 'equal?'."
+  (fold-right (lambda (elem ret)
+                (let ((entry (assoc (car elem) ret)))
+                  (if entry
+                      (acons (car elem)
+                             (append (ensure-list (cdr elem))
+                                     (ensure-list (cdr entry)))
+                             (alist-delete! (car elem) ret))
+                      (cons elem ret))))
+              '()
+              (apply append lst1 rest)))
+
+(define (file-name-concat dir . rest)
+  "Concatenate DIR and REST filename components.  Any final slashes are
+stripped from the resulting filename."
+  (string-join (append (list (string-trim-right dir #\/))
+                       (map (cut string-trim-both <> #\/)
+                            rest))
+               "/"))
+
+(define emacs-config-filename
+  (cut file-name-concat "emacs" <>))
+
+(define (composite-file name . files)
+  "Return an object representing store file NAME containing the text contents
+of all file-like objects in FILES."
+  (define builder
+    (with-imported-modules (source-module-closure
+                            '((ice-9 binary-ports)
+                              (rnrs bytevectors)))
+      #~(begin
+          (call-with-output-file #$output:out
+            (lambda (port)
+              (set-port-encoding! port "UTF-8")
+              #$@(interpose
+                  (map
+                   (lambda (file)
+                     ;; XXX: 'get-bytevector-all' returns <#eof> when the port
+                     ;; is empty.
+                     #~(let ((bv (call-with-input-file #$file
+                                   (@ (ice-9 binary-ports)
+                                      get-bytevector-all))))
+                         ((@ (ice-9 binary-ports) put-bytevector) port
+                          (if (eof-object? bv)
+                              ((@ (rnrs bytevectors) make-bytevector) 0)
+                              bv))))
+                   files)
+                  #~(display "\n")
+                  'suffix))))))
+
+  (computed-file name builder))
+
+
+;;;
+;;; Emacs home service
+;;;
+
+(define %default-emacs emacs)
+(define %default-emacs-config-dir "~/.config/emacs/")
+(define %emacs-user-init-filename "init.el")
+(define %emacs-early-init-filename "early-init.el")
+
+(define list-of-file-likes?
+  (list-of file-like?))
+
+(define (elisp-gexp-or-file-like? val)
+  (or (elisp? val)
+      (list? val)                       ; Allow using s-exps internally.
+      (gexp? val)
+      (file-like? val)))
+
+(define list-of-elisp-gexp-or-file-likes?
+  (list-of elisp-gexp-or-file-like?))
+
+(define-configuration/no-serialization emacs-server
+  (name
+   (string)
+   "A string naming the server.  Users will subsequently be able to start the
+new server by using the command @code{herd start emacs-@var{name}}.  To create
+Emacs client frames for the sever, users can use commands like:
+@code{emacsclient --create-frame --socket-name=@var{name}}.
+
+Because this string is meant for use in shell commands (and filenames), it
+should not contain any characters other than letters and digits and the
+characters @samp{-}, @samp{_}, and @samp{.}."
+   (sanitizer
+    (lambda (str)
+      (cond
+       ((not (string? str))
+        (configuration-field-error #f 'name str))
+       ((string-any (char-set-complement
+                     (char-set-union char-set:letter+digit
+                                     (char-set #\- #\_ #\.)))
+                    str)
+        (configuration-field-error #f 'name str))
+       (else str)))))
+  (inherit-user-directory?
+   (boolean #t)
+   "Whether the server should share its Emacs user directory with that of
+the Emacs Home service.  When false, the server will use a subdirectory
+of the one used by the service for its own user directory.  When true
+(the default), the @code{user-emacs-directory} Emacs variable for the server
+will be set to that of the Emacs Home service, but the server will still load
+its own @file{early-init.el} and @file{init.el} files.  See the
+@code{inherit-init-files?} field for how to inherit configuration from other
+Emacsen.")
+  (inherit-init-files?
+   (boolean #t)
+   "Whether to load the default configuration used by the Emacs Home service,
+that is, the initialization expressions specified by the
+@code{home-emacs-configuration} value for the Emacs Home service.  These are
+loaded in addition to any configuration specified for this specific server.
+
+Note that if this field is true and @code{inherit-user-directory?} is false,
+duplicate copies of of any files specified by the @code{user-files} field of
+the @code{home-emacs-configuration} value for the service are created in the
+Emacs user directory for the server.  This ensures that any references to
+those files in the inherited configuration expressions will not fail in
+unexpected ways.")
+  (auto-start?
+   (boolean #t)
+   "Whether to start the server automatically.")
+  (debug?
+   (boolean #f)
+   "Whether to enable the Emacs Lisp debugger for errors in the initialization
+files of the server.")
+  (shepherd-requirements
+   (list-of-symbols '())
+   "A list of symbols specifying Shepherd services that must be started before
+the service for the Emacs server can be started (@pxref{Defining Services,,,
+shepherd, The GNU Shepherd Manual}).")
+  (environment-variables
+   (gexp #~(default-environment-variables))
+   "A G-expression specifying a list environment variables for the Emacs server.
+The value of this field, if set, will override all default environment
+variables, and users should likely specify a G-expression whose expansion
+includes elements from @code{default-environment-variables} (@pxref{Service
+De- and Constructors,,, shepherd, The GNU Shepherd Manual}).")
+  (load-custom?
+   (boolean #t)
+   "Whether to load customizations created with the Emacs customization
+interface.  When @code{inherit-directory?} is true, customizations made within
+this specific server affect other Emacsen, and vice versa.  Otherwise, the
+server has its own separate set of customizations.")
+  (early-init-file
+   (list-of-elisp-gexp-or-file-likes '())
+   "A list of Elisp expressions, G-expressions, or file-like objects to
+serialize to the Emacs early init file, the @file{early-init.el} file in the
+server's Emacs configuration directory.  For file-like objects that represent
+actual files in the store (for example, those returned by @code{local-file} or
+@code{plain-file}), the contents of each file are serialized.
+
+When the @code{inherit-init-files?} for the server is true, configuration
+specified here is serialized to the early init file after any early init file
+configuration expressions specified by the Emacs Home service type.")
+  (init-file
+   (list-of-elisp-gexp-or-file-likes '())
+   "A list Elisp expressions, G-expressions, or file-like objects to serialize
+to the Emacs user initialization file, the @file{init.el} file in the server's
+Emacs configuration directory.  For file-like objects that represent actual
+files in the store (for example, those returned by @code{local-file} or
+@code{plain-file}), the contents of each file are serialized.
+
+When the @code{inherit-init-files?} for the server is true, configuration
+specified here is serialized to the Emacs user initialization file after the
+any user initialization file configuration expressions specified by the Emacs
+Home service type.")
+  (user-files
+   (alist '())
+   "An association list of filenames and file-like objects specifying files to
+create in the server's Emacs user directory.  For each entry, a file with the
+given filename will be created with the contents of the file-like object.  If
+a list of file-like objects is given for an entry, the new file will contain
+the combined text contents of all of the file-like objects in the list.  This
+field can be used to add configuration files for Emacs that should not be
+automatically loaded when Emacs is initialized.
+
+Note that the Emacs user directory for the server may not be the same as the
+directory containing its Emacs user initialization file.
+
+When the @code{inherit-user-directory?} field is false, user files are created
+in a subdirectory of the user directory used by the Emacs Home service, which
+will be populated with copies of all of files specified in the
+@code{user-files} field of the @code{home-emacs-configuration} value for the
+service."
+   (sanitizer (alist-sanitizer user-files
+                               ((? string?)
+                                . (or (? file-like?)
+                                      (? list-of-file-likes?)))))))
+
+(define list-of-emacs-servers?
+  (list-of emacs-server?))
+
+(define-configuration/no-serialization home-emacs-configuration
+  (emacs
+   (package %default-emacs)
+   "The package providing the @file{/bin/emacs} command.")
+  (packages
+   (list-of-file-likes '())
+   "A list of additional Emacs-related packages to install.")
+  (user-emacs-directory
+   (string %default-emacs-config-dir)
+   "Directory beneath which additional Emacs user files are placed.
+By default, this is also the directory that contains the @file{init.el} and
+@file{early-init.el} Emacs initialization files, but you can change this field
+to specify any directory of your choosing; initialization files generated by
+this service will still be loaded."
+   (sanitizer
+    (lambda (str)
+      ;; Ensure that the path name ends with a '/', as some low-level Emacs
+      ;; libraries use the value of 'user-emacs-directory' with this
+      ;; expectation.
+      (cond
+       ((not (string? str))
+        (configuration-field-error #f 'user-emacs-directory str))
+       ((not (string-suffix? "/" str))
+        (string-append str "/"))
+       (else str)))))
+  (native-compile?
+   (boolean #f)
+   "Whether to enable native-compilation of Emacs packages by building them
+with the Emacs specified by the @code{emacs} field rather than
+@code{emacs-minimal}.")
+  (indent-forms
+   (alist '())
+   "An association list of symbols and indentation rules.  Each entry is of
+the form (@var{symbol} .  @var{indent}), where @var{symbol} is a symbol and
+@var{indent} is an integer.
+
+When @var{symbol} occurs at the beginning of a list in an Emacs Lisp file, the
+first @var{indent} expressions are indented as arguments and the remainder as
+body expressions, as if @var{indent} was supplied as the
+@code{lisp-indent-function} symbol property for @var{symbol} in Emacs.
+Argument expressions are either printed on the same line as @var{symbol} or
+indented 4 columns beyond the base indentation of the enclosing list, and body
+expressions are indented 2 columns beyond the base indentation."
+   (sanitizer (alist-sanitizer indent-forms
+                               ((? symbol?) . (? integer?)))))
+  (load-custom?
+   (boolean #t)
+   "Whether to load customizations created with the Emacs customization
+interface.  Because all configuration files created by this service are
+effectively read-only, the service modifies the default behavior of Emacs so
+that customizations are always saved in a separate @file{custom.el} file,
+which will be loaded when Emacs is initialized if this field is true.")
+  (early-init-file
+   (list-of-elisp-gexp-or-file-likes '())
+   "A list of Elisp expressions, G-expressions, or file-like objects to
+serialize to the Emacs early init file, the @file{early-init.el} file in the
+appropriate Emacs configuration directory.  For file-like objects that
+represent actual files in the store (for example, those returned by
+@code{local-file} or @code{plain-file}), the contents of each file are
+serialized.
+
+The Emacs home service automatically serializes some s-expressions to the
+early initialization file in order to ensure that the Emacs user directory is
+properly set according to the @code{user-emacs-directory} field.  Any
+expressions specified in this field are serialized before the automatically
+added expressions.  This comes with a trade-off: one the one hand, you can
+leverage this fact to insert header comments or a license preamble at the
+beginning of the file, where it would be expected, but on the other hand, the
+Emacs user initialization directory will not be set until after any
+s-expressions specified in this field have already been evaluated.")
+  (init-file
+   (list-of-elisp-gexp-or-file-likes '())
+   "A list additional of Elisp expressions, G-expressions, or file-like
+objects to serialize to the Emacs user initialization file, the @file{init.el}
+file in the appropriate Emacs configuration directory.  For file-like objects
+that represent actual files in the store (for example, those returned by
+@code{local-file} or @code{plain-file}), the contents of each file are
+serialized.
+
+As with the @code{early-init-file} field, some expressions are automatically
+serialized to the Emacs user initialization file by the Emacs home service,
+particularly those to set up the @file{custom.el} file (see the
+@code{load-custom?} field).  Expressions specified using this field are
+serialized before any automatically added expressions.  This means that you
+can specify header comments or license preambles that will occur at the
+beginning of the file, but that the @file{custom.el} file will be loaded after
+all expressions in this field are evaluated.")
+  (user-files
+   (alist '())
+   "An association list of filenames and file-like objects specifying files to
+create in the Emacs user directory.  For each entry, a file with the given
+filename will be created with the contents of the file-like object.  If a list
+of file-like objects is given for an entry, the new file will contain the
+combined text contents of all of the file-like objects in the list.  This
+field can be used to add configuration files for Emacs that should not be
+automatically loaded when Emacs is initialized.
+
+Note that the Emacs user directory, which can be specified using the
+@code{user-emacs-directory} field, may not be the same as the directory
+containing the Emacs user initialization file."
+   (sanitizer (alist-sanitizer user-files
+                               ((? string?)
+                                . (or (? file-like?)
+                                      (? list-of-file-likes?))))))
+  (servers
+   (list-of-emacs-servers '())
+   "A list of configurations for Emacs servers."))
+
+(define-configuration/no-serialization home-emacs-extension
+  (packages
+   (list-of-file-likes '())
+   "A list of additional Emacs-related packages to install.")
+  (indent-forms
+   (alist '())
+   "An association list of symbols and indentation rules.  Each entry is of
+the form (@var{symbol} . @var{indent}), where @var{symbol} is a symbol and
+@var{indent} is an integer specifying the number of argument expressions for
+@var{symbol}."
+   (sanitizer (alist-sanitizer indent-forms
+                               ((? symbol?) . (? integer?)))))
+  (early-init-file
+   (list-of-elisp-gexp-or-file-likes '())
+   "A list of Elisp expressions, G-expressions, or file-like objects to
+serialize to the Emacs early init file, the @file{early-init.el} file in the
+Emacs configuration directory.")
+  (init-file
+   (list-of-elisp-gexp-or-file-likes '())
+   "A list Elisp expressions, G-expressions, or file-like objects to serialize
+to the Emacs user initialization file, the @file{init.el} file in the Emacs
+configuration directory.")
+  (user-files
+   (alist '())
+   "An association list of filenames and file-like objects specifying files to
+create in the Emacs user directory.
+
+Note that the Emacs user directory may not be the same as the directory
+containing the Emacs user initialization file."
+   (sanitizer (alist-sanitizer user-files
+                               ((? string?)
+                                . (or (? file-like?)
+                                      (? list-of-file-likes?))))))
+  (servers
+   (list-of-emacs-servers '())
+   "A list of configurations for Emacs servers.  You cannot specify multiple
+@code{emacs-server} objects with equivalent @code{name} fields."))
+
+(define (server-name->file-name name)
+  "Return the full name for server NAME as a filename."
+  (string-append "emacs-" (string-delete (char-set #\/ #\nul) name)))
+
+(define (emacs-server->provision config)
+  "Return the provision symbol for the Shepherd service created for
+@code{emacs-server} object CONFIG."
+  (match-record config <emacs-server>
+                (name)
+    (string->symbol (server-name->file-name name))))
+
+(define (server-user-directory name user-emacs-directory)
+  "Return the location of the Emacs user directory for server NAME based on
+the USER-EMACS-DIRECTORY from the Emacs home service."
+  (string-append (file-name-concat user-emacs-directory
+                                   (server-name->file-name name))
+                 "/"))
+
+(define (home-emacs-packages config)
+  "Return a list of file-like objects to install from CONFIG."
+  ;; XXX: This can cause some unforeseen problems, since most packages have
+  ;; not been tested with input rewriting.
+  (define (rewrite-for-native-compile emacs)
+    (package-input-rewriting
+     `((,emacs-minimal . ,emacs))))
+
+  (match-record config <home-emacs-configuration>
+                (emacs
+                 native-compile?
+                 packages)
+    (append (list emacs)
+            (if native-compile?
+                (map (rewrite-for-native-compile emacs)
+                     (delete-duplicates packages eq?))
+                packages))))
+
+(define (home-emacs-xdg-configuration-files config)
+  "Return from CONFIG an association list of filenames and file-like objects
+to create in XDG_CONFIG_HOME."
+  (define (elisp-file-with-forms name exps)
+    (composite-elisp-file name exps
+                          #:special-forms
+                          (home-emacs-configuration-indent-forms config)))
+
+  (define (set-user-emacs-directory-sexps directory)
+    (list `(setq user-emacs-directory ,directory)
+          ;; Reset variables set before early init file is loaded that rely
+          ;; upon the value of `user-emacs-directory'.
+          ;;
+          ;; XXX: 'native-comp-eln-load-path' is properly set in startup.el to
+          ;; reflect the new 'user-emacs-directory', but this means that
+          ;; servers which use their own 'user-emacs-directory' get their own
+          ;; eln cache.
+          '(custom-reevaluate-setting 'auto-save-list-file-prefix)
+          '(custom-reevaluate-setting 'package-user-dir)
+          '(custom-reevaluate-setting 'package-quickstart-file)
+          '(custom-reevaluate-setting 'abbrev-file-name)
+          '(custom-reevaluate-setting 'custom-theme-directory)))
+
+  (define (load-custom?-sexps load-custom?)
+    ;; 'locate-user-emacs-file' also ensures that 'user-emacs-directory'
+    ;; exists, creating it with the proper permissions as a side effect.
+    (list '(setq custom-file (locate-user-emacs-file "custom.el"))
+          (if load-custom?
+              '(if (not (file-exists-p custom-file))
+                   (make-empty-file custom-file)
+                   (load custom-file))
+              '(when (not (file-exists-p custom-file))
+                 (make-empty-file custom-file)))
+          (elisp (unelisp-newline))))
+
+  (define config-user-emacs-directory
+    (home-emacs-configuration-user-emacs-directory config))
+
+  (define (server->config-file-entries server
+                                       inherited-early-init-file
+                                       inherited-init-file)
+    (match-record server <emacs-server>
+                  (name
+                   inherit-init-files?
+                   inherit-user-directory?
+                   load-custom?
+                   early-init-file
+                   init-file)
+      (let ((server-dir (server-name->file-name name)))
+        (list (list (emacs-config-filename
+                     (file-name-concat server-dir %emacs-early-init-filename))
+                    (elisp-file-with-forms %emacs-early-init-filename
+                                           (append
+                                            (set-user-emacs-directory-sexps
+                                             (if inherit-user-directory?
+                                                 config-user-emacs-directory
+                                                 (server-user-directory
+                                                  name
+                                                  config-user-emacs-directory)))
+                                            (if inherit-init-files?
+                                                inherited-early-init-file
+                                                '())
+                                            early-init-file)))
+              (list (emacs-config-filename
+                     (file-name-concat server-dir %emacs-user-init-filename))
+                    (elisp-file-with-forms %emacs-user-init-filename
+                                           (append
+                                            (if inherit-init-files?
+                                                inherited-init-file
+                                                '())
+                                            init-file
+                                            (load-custom?-sexps load-custom?))))))))
+
+  (match-record config <home-emacs-configuration>
+                (user-emacs-directory
+                 load-custom?
+                 early-init-file
+                 init-file
+                 servers)
+    (append (list (list (emacs-config-filename %emacs-early-init-filename)
+                        (elisp-file-with-forms %emacs-early-init-filename
+                                               (append
+                                                early-init-file
+                                                (set-user-emacs-directory-sexps
+                                                 user-emacs-directory))))
+                  (list (emacs-config-filename %emacs-user-init-filename)
+                        (elisp-file-with-forms %emacs-user-init-filename
+                                               (append init-file
+                                                       (load-custom?-sexps
+                                                        load-custom?)))))
+            (append-map (cut server->config-file-entries <>
+                             early-init-file
+                             init-file)
+                        servers))))
+
+(define (home-emacs-files config)
+  "Return from CONFIG an association list of filenames and file-like objects
+to create in the Emacs user directory."
+
+  (define file-name-with-home (make-regexp "^(~/|/home/[^/]+/)(.+)$"))
+
+  (define (file-name->home-file-name filename)
+    (or (and=> (regexp-exec file-name-with-home filename)
+               (cut match:substring <> 2))
+        filename))
+
+  (define (user-file->home-files-entry entry directory)
+    (match entry
+      ((name . files)
+       (list (file-name-concat directory name)
+             (apply composite-file (basename name) (ensure-list files))))))
+
+  (define (server->home-files-entries server directory)
+    (match-record server <emacs-server>
+                  (name
+                   inherit-user-directory?
+                   inherit-init-files?
+                   user-files)
+      (let ((server-dir (if inherit-user-directory?
+                            directory
+                            (server-user-directory name directory))))
+        (map (cut user-file->home-files-entry <> server-dir)
+             (append user-files
+                     (if (and inherit-init-files?
+                              (not inherit-user-directory?))
+                         (home-emacs-configuration-user-files config)
+                         '()))))))
+
+  (match-record config <home-emacs-configuration>
+                (user-emacs-directory
+                 user-files
+                 servers)
+    (let ((user-emacs-directory* (file-name->home-file-name
+                                  user-emacs-directory)))
+      (append (map (cut user-file->home-files-entry <> user-emacs-directory*)
+                   user-files)
+              (append-map (cut server->home-files-entries <>
+                               user-emacs-directory*)
+                          servers)))))
+
+(define (home-emacs-shepherd-services config)
+  "Return a list of Shepherd services for CONFIG."
+  (match-record config <home-emacs-configuration>
+                (emacs user-emacs-directory servers)
+    (map
+     (lambda (server)
+       (match-record server <emacs-server>
+                     (name
+                      inherit-user-directory?
+                      auto-start?
+                      debug?
+                      shepherd-requirements
+                      environment-variables)
+         (let ((server-init-dir (file-name-concat
+                                 %default-emacs-config-dir
+                                 (server-name->file-name name)))
+               (server-user-dir (if inherit-user-directory?
+                                    user-emacs-directory
+                                    (server-user-directory
+                                     name user-emacs-directory))))
+           (shepherd-service
+            (provision (list (emacs-server->provision server)))
+            (requirement shepherd-requirements)
+            (start
+             #~(make-forkexec-constructor
+                (list #$(file-append emacs "/bin/emacs")
+                      #$(string-append "--init-directory=" server-init-dir)
+                      #$(string-append "--fg-daemon=" name)
+                      #$@(if debug?
+                             (list "--debug-init")
+                             '()))
+                #:environment-variables
+                #$environment-variables
+                #:log-file
+                #$(file-name-concat server-user-dir
+                                    (string-append
+                                     (server-name->file-name name) ".log"))))
+            (stop
+             #~(make-forkexec-constructor
+                (list #$(file-append emacs "/bin/emacsclient")
+                      "-s" #$name "--eval" "(kill-emacs)")))
+            (actions (list
+                      (shepherd-configuration-action
+                       (file-name-concat server-init-dir
+                                         %emacs-user-init-filename))))
+            (auto-start? auto-start?)
+            (documentation
+             (string-append "Start the Emacs server called "
+                            name "."))))))
+     servers)))
+
+(define (home-emacs-extensions original extensions)
+  "Extend the Emacs home service configuration ORIGINAL with list of
+configurations EXTENSIONS."
+  (match-record original <home-emacs-configuration>
+                (packages
+                 indent-forms
+                 early-init-file
+                 init-file
+                 user-files
+                 servers)
+    (home-emacs-configuration
+     (inherit original)
+     (packages (append packages
+                       (append-map home-emacs-extension-packages
+                                   extensions)))
+     (indent-forms (append indent-forms
+                           (append-map home-emacs-extension-indent-forms
+                                       extensions)))
+     (early-init-file (append early-init-file
+                              (append-map home-emacs-extension-early-init-file
+                                          extensions)))
+     (init-file (append init-file
+                        (append-map home-emacs-extension-init-file
+                                    extensions)))
+     (user-files (merge-alists user-files
+                              (append-map home-emacs-extension-user-files
+                                          extensions)))
+     ;; XXX: Does not handle servers with duplicate names.  Will Shepherd
+     ;; signal an error to notify the user?
+     (servers (append servers
+                      (append-map home-emacs-extension-servers
+                                  extensions))))))
+
+(define home-emacs-service-type
+  (service-type (name 'home-emacs-service)
+                (extensions
+                 (list (service-extension
+                        home-profile-service-type
+                        home-emacs-packages)
+                       (service-extension
+                        home-xdg-configuration-files-service-type
+                        home-emacs-xdg-configuration-files)
+                       (service-extension
+                        home-files-service-type
+                        home-emacs-files)
+                       (service-extension
+                        home-shepherd-service-type
+                        home-emacs-shepherd-services)))
+                (default-value (home-emacs-configuration))
+                (compose identity)
+                (extend home-emacs-extensions)
+                (description
+                 "Configure and run the GNU Emacs extensible text editor.")))
+
+
+;;;
+;;; Emacs packages home service
+;;;
+
+(define %emacs-extra-init-files-path %default-emacs-config-dir)
+(define %emacs-extra-init-files-path-variable 'guix--extra-files-load-directory)
+
+(define (elisp-or-gexp? val)
+  (or (elisp? val)
+      (gexp? val)))
+
+(define (keyword-or-symbol? val)
+  (or (keyword? val)
+      (symbol? val)))
+
+(define (package-or-null? val)
+  (or (package? val)
+      (null? val)))
+
+(define (string-or-file-like? val)
+  (or (string? val)
+      (file-like? val)))
+
+(define (string-or-vector? val)
+  (or (string? val)
+      (vector? val)))
+
+(define (symbol-or-false? val)
+  (or (symbol? val)
+      (not val)))
+
+(define list-of-elisp-or-gexps?
+  (list-of elisp-or-gexp?))
+
+(define list-of-string-or-file-likes?
+  (list-of string-or-file-like?))
+
+(define (elispifiable-quoted? val)
+  "Return whether VAL can be serialized as Elisp, but needs to be quoted."
+  (or (symbol? val)
+      (pair? val)))
+
+(define (elispifiable? val)
+  "Return whether VAL can be serialized as Elisp."
+  (or (constant? val)
+      (elispifiable-quoted? val)
+      (gexp? val)
+      (file-like? val)
+      (and (elisp? val)
+           (not (blank? (elisp->sexp val))))))
+
+(define elispifiable->elisp
+  (match-lambda
+    ((? elisp? obj)
+     obj)
+    ((? elispifiable-quoted? obj)
+     (sexp->elisp `(quote ,obj)))
+    (obj
+     (sexp->elisp obj))))
+
+(define-syntax keys-field-sanitizer
+  (syntax-rules ()
+    ((_ field-name)
+     (alist-sanitizer field-name
+                      ((? string-or-vector?) . (? symbol-or-false?))))))
+
+(define-configuration/no-serialization emacs-keymap
+  (name
+   (symbol 'global-map)
+   "The symbol of the Emacs keymap in which to bind keys.")
+  (package-name
+   (symbol-or-false #f)
+   "The symbol naming the Emacs package providing the keymap, as would be used
+with Emacs @code{require}.  If this field is false (the default), then the
+package for which the keymap is being configured should define the keymap or
+the keymap should otherwise be defined by the time the configuration for the
+package is evaluated.  Note that when the @code{prefix?} field is true, this
+implies that the keymap will be defined and this field has no effect.")
+  (prefix?
+   (boolean #f)
+   "Whether to create a new keymap and assign it to a prefix command named
+@code{name}.  Specifically, the new keymap is assigned to the function
+definition of the symbol @code{name}.  This is useful when the keymap does not
+already exist, but the user would like to bind it to a prefix key using the
+@code{keys-global} field in an @code{emacs-package} configuration object or
+even using the @code{keys} field in another @code{emacs-keymap} object.
+Because a prefix command is a valid function, no special handling is needed to
+use it in keybinding definitions where commands are expected.  Another benefit
+of prefix commands comes when using keybinding introspection features such as
+@code{which-key-mode}: @code{name} will be displayed as a prefix key
+description when it is bound to a prefix command declared in this way, whereas
+ad-hoc prefix key bindings like @kbd{C-c d d} where @kbd{C-c d} is not
+explicitly defined will not have explicitly meaningful descriptions.
+
+Note that when this field true, the @code{package-name} field has no effect.")
+  (repeat?
+   (boolean #f)
+   "Whether to make this keymap a repeat map (@pxref{Repeating,,, emacs, The
+GNU Emacs Manual}).  Repeat maps are created by setting the @code{repeat-map}
+symbol property for each key definition in @code{keys} to the @code{name} of
+this keymap.  Use the @code{repeat-exit} field to override this setting for
+specific bindings.")
+  (repeat-exit
+   (list-of-symbols '())
+   "A list of commands that exit the repeat map.  When @code{repeat?} is true,
+these commands do not get the @code{repeat-map} property.  The meaning of this
+field is similar to that of the @code{:exit} keyword used by the
+@code{defvar-keymap} function in Emacs.  This field has no effect when
+@code{repeat?} is false.")
+  (repeat-enter
+   (list-of-symbols '())
+   "A list of additional commands that enter the repeat map.  When
+@code{repeat?} is true, these commands get the @code{repeat-map} property,
+even when they are not bound in the keymap.  This is only useful when a
+command is not bound in @code{name}, but the repeat map should be accessible
+after that command is invoked (e.g., with @kbd{M-x}).  The meaning of this
+field is similar to that of the @code{:enter} keyword used by the
+@code{defvar-keymap} function in Emacs.  This field has no effect when
+@code{repeat?} is false.")
+  (disabled-commands
+   (alist '())
+   "An association list of command symbols and whether to disable them.  When
+a disabled command is interactively invoked, Emacs asks for confirmation from
+the user (@pxref{Disabling,,, emacs, The GNU Emacs Manual}).  The values of
+this alist should be booleans, which will be stored as the value of the
+@code{disabled} property of each respective command symbol.  Thus, to disable
+the @code{transpose-chars} command and enable the @code{erase-buffer} command,
+you can use:
+
+@lisp
+'((transpose-chars . #t)
+  (erase-buffer . #f))
+@end lisp
+"
+   (sanitizer
+    (alist-sanitizer disabled-commands
+                     ((? symbol?) . (? boolean?)))))
+  (keys
+   (alist '())
+   "An association list of key sequences and binding definitions.  Key
+sequences are Emacs-specific string or vector representations of sequences of
+keystrokes or events.  Strings should be valid arguments to the Emacs function
+@code{kbd}, and they are preferred over the low-level vector
+representations (@pxref{Keymaps,,, elisp, The Emacs Lisp Manual}).  Binding
+definitions should be Emacs command symbols.  As a special case, when a
+binding definition is the boolean false, the key is unset in the keymap."
+   (sanitizer (keys-field-sanitizer keys))))
+
+(define list-of-emacs-keymaps?
+  (list-of emacs-keymap?))
+
+(define-configuration/no-serialization emacs-package
+  (name
+   (symbol)
+   "The symbol naming the Emacs package or library, as would be used with
+Emacs @code{require}.")
+  (package
+    (package-or-null '())
+    "A Guix package providing the Emacs package specified by @code{name}.  If
+the package is built into Emacs, or if there is no associated Guix package,
+this field should be set to the empty list (the default).")
+  (extra-packages
+   (list-of-packages '())
+   "A list of packages that provide additional functionality used by this
+package, but which are not installed automatically by the Guix package manager
+as propagated inputs of @code{package}.")
+  (install?
+   (boolean #t)
+   "Whether to install @code{package} and @code{extra-packages}.")
+  (extra-init-files
+   (alist '())
+   "An association list of filenames and file-like objects containing Emacs
+Lisp to load when Emacs is initialized.  For each entry, a file with the text
+contents of the file-like object, or the combined text contents of all of the
+file-like objects in a list if a list is specified, will be created with the
+given filename in the appropriate Emacs configuration directory (the directory
+where the @file{early-init.el} and @file{init.el} files are located).  These
+files will then be loaded when Emacs is initialized.
+
+This can be used as an escape hatch, similarly to the @code{extra-init} field.
+It is useful when large amounts of code must be loaded in order to configure a
+package.
+
+Note that it is an error to specify files with the filenames @samp{init.el}
+and @samp{early-init.el}, because these files are already generated by the
+Emacs home service.
+
+Also note that it is up to the package serializer whether and when to load
+these files, although the @code{home-emacs-service-type} ensures that the
+files will be present in the relevant configuration directories.
+@code{%emacs-simple-package-serializer} and
+@code{%emacs-use-package-serializer} both load the files at initialization
+time, before any expressions given in the @code{extra-init} field, regardless
+of the value of the @code{load-after-packages} field."
+   (sanitizer (alist-sanitizer extra-init-files
+                               ((? string?)
+                                . (or (? file-like?)
+                                      (? list-of-file-likes?))))))
+  (user-files
+   (alist '())
+   "An association list of filenames and file-like objects specifying files to
+create in the Emacs user directory.  For each entry, a file with the given
+filename will be created in the Emacs user directory with the contents of the
+file-like object.  If a list of file-like objects is given for an entry, the
+new file will contain the combined text contents of all of the file-like
+objects in the list.  This field should be used to add per-package files to
+the Emacs user directory."
+   (sanitizer (alist-sanitizer user-files
+                               ((? string?)
+                                . (or (? file-like?)
+                                      (? list-of-file-likes?))))))
+  (load-force?
+   (boolean #f)
+   "Whether to force loading of this package immediately when Emacs is
+initialized, rather than deferring loading, for example, until an autoloaded
+function is invoked.  This is similar in effect to the keyword @code{:demand}
+from @code{use-package} and to the inverse of the keyword @code{:defer}.  The
+difference is that when this field is false, package loading should always be
+deferred; @code{use-package} normally does not defer loading when it does not
+set up autoloads, because it doesn't know that Guix handles autoloads on its
+own.")
+  (load-predicates
+   (list-of-elisp-or-gexps '())
+   "A list predicate expressions to evaluate when Emacs is initialized to
+determine whether to evaluate the configuration for this package.  When this
+list is not empty, @emph{all} other configuration for this package should be
+effectively surrounded in the Emacs user initialization file by a block of the
+form: @code{(when @var{load-predicates} @dots{})}.  This is the supercharged
+Guix version of the @code{use-package} @code{:if} keyword!
+
+If multiple load predicates are specified, the behavior is determined by the
+package configuration serializer.  Both
+@code{%emacs-simple-package-serializer} and
+@code{%emacs-use-package-serializer} compose load predicates using @code{and},
+so that all load predicates in the list must be satisfied in order for the
+package configuration to be evaluated.")
+  (load-after-packages
+   (list-of-symbols '())
+   "A list of symbols for Emacs packages that must be loaded before this
+package is loaded.  Only after all of the packages in the list have been
+loaded by Emacs should configuration for this package be evaluated.  This is
+similar to a simplified version of the @code{:after} keyword from
+@code{use-package}.")
+  (load-paths
+   (list-of-string-or-file-likes '())
+   "A list of additional load paths to add to the Emacs @code{load-paths}
+variable.  Load paths can be specified either as strings or as file-like
+objects, in which case a path to the respective store item is substituted.")
+  (autoloads
+   (list-of-symbols '())
+   "A list of Emacs functions from the package to autoload.  This can be
+useful, for example, when defining custom commands in the Emacs user
+initialization file that use functions which are not autoloaded by default.")
+  (autoloads-interactive
+   (list-of-symbols '())
+   "A list of additional Emacs interactive commands from the package to
+autoload, so that they can be invoked interactively before the package is
+loaded.")
+  (keys-global
+   (alist '())
+   "An association list of key sequences (as strings or vectors) and Emacs
+commands to bind in the global keymap."
+   (sanitizer (keys-field-sanitizer keys-global)))
+  (keys-global-keymaps
+   (alist '())
+   "An association list of key sequences and Emacs keymap variables to bind to
+them in the global keymap.  The keymap variables should be symbols that define
+keymaps in the package; they can be effectively autoloaded using this
+assumption."
+   (sanitizer (alist-sanitizer field-name
+                               ((? string-or-vector?) . (? symbol?)))))
+  (keys-override
+   (alist '())
+   "An association list of key sequences and symbols naming Emacs commands to
+bind in the global override map.  These key bindings have a higher precedence
+than local and global keybindings."
+   (sanitizer (keys-field-sanitizer keys-override)))
+  (keys-local
+   (list-of-emacs-keymaps '())
+   "A list of key binding configurations for specific keymaps, each contained
+in an @code{emacs-keymap} object.")
+  (options
+   (alist '())
+   "An association list of user options and values for this package.  Options
+should be symbols naming Emacs variables, and values can be any object that
+can be serialized to Elisp.  For values, primitive Scheme data types are
+implicitly quoted, including lists and symbols.  To instead set an option to
+the value of an expression to be evaluated at Emacs initialization time,
+either use an Elisp expression (e.g., specified with the @code{elisp} form) or
+a G-expression for a value."
+   (sanitizer (alist-sanitizer options
+                               ((? symbol?) . (? elispifiable?)))))
+  (faces
+   (alist '())
+   "An association list of face symbols and face specs.  @xref{Defining
+Faces,,, elisp, The Emacs Lisp Manual} for the format of face specs."
+   (sanitizer (alist-sanitizer
+               faces
+               ((? symbol?)
+                . (((or 'default #t 't (? list?)) . (prop . rest)) ..1)))))
+  (hooks
+   (alist '())
+   "An association list of hooks and functions to add to them.  Each entry is
+a pair of symbols.  Hook symbols in Emacs should end in @samp{-hook}, but the
+@code{%emacs-simple-package-serializer} and
+@code{%emacs-use-package-serializer} serializers effectively add this suffix
+when necessary."
+   (sanitizer (alist-sanitizer hooks
+                               ((? symbol?) . (? symbol?)))))
+  (auto-modes
+   (alist '())
+   "An association list of filename patterns as regular expression strings and
+Emacs mode functions to call when visiting files with filenames that match the
+patterns.  @xref{Auto Major Mode,,, elisp, The Emacs Lisp Manual} for
+details."
+   (sanitizer (alist-sanitizer auto-modes
+                               ((? string?) . (? symbol?)))))
+  (magic-modes
+   (alist '())
+   "An association list regular expression strings and Emacs mode functions to
+call when visiting files that begin with matching text.  @xref{Auto Major
+Mode,,, elisp, The Emacs Lisp Manual} for details."
+   (sanitizer (alist-sanitizer magic-modes
+                               ((? string?) . (? symbol?)))))
+  (extra-after-load
+   (list-of-elisp-or-gexps '())
+   "A list of Elisp expressions or G-expressions to evaluate after the package
+is loaded, as with the Emacs @code{eval-after-load} function.  Elisp
+expressions can be specified using the @code{elisp} and @code{elisp*} forms.")
+  (extra-init
+   (list-of-elisp-or-gexps '())
+   "A list of Elisp expressions or G-expressions to evaluate immediately when
+Emacs is initialized, even if loading is deferred due to the
+@code{load-force?} field.  Note that the @code{load-predicates} field should
+still determine whether these expressions are evaluated, and they will only be
+evaluated after all packages specified in the @code{load-after-packages} field
+have been loaded.")
+  (extra-keywords
+   (alist '())
+   "An association list of keys and lists of extra Elisp expressions or
+G-expressions.  Keys can potentially be any keyword or symbol object; keywords
+are automatically serialized to their Emacs Lisp equivalent (e.g.,
+@code{#:keyword} is serialized as @code{:keyword}).  The meanings of entries
+is specific to each package serializer, and any key may be ignored by a
+package serializer.  This field is currently ignored by the
+@code{%emacs-simple-package-serializer}.  Entries in this list matching
+@code{use-package} keywords will be spliced by the
+@code{%emacs-use-package-serializer} into the @code{use-package} body, after
+all other forms."
+   (sanitizer (alist-sanitizer extra-keywords
+                               ((? keyword-or-symbol? key)
+                                . (? list-of-elisp-or-gexps? val))))))
+
+(define list-of-emacs-packages?
+  (list-of emacs-package?))
+
+(define-configuration/no-serialization emacs-package-serializer
+  (name
+   (symbol)
+   "A symbol identifying the serializer.")
+  (procedure
+   (procedure)
+   "A procedure that takes one argument, an @code{emacs-package} object, and
+that should return a list of @code{elisp} objects or G-expressions containing
+configuration to serialize to the Emacs user initialization file.")
+  (dependencies
+   (list-of-packages '())
+   "An list of additional packages to install whenever this
+serializer is used.")
+  (indent-forms
+   (alist '())
+   "An association list of symbols and indentation rules.  Each entry is of
+the form (@var{symbol} .  @var{indent}), where @var{symbol} is a symbol and
+@var{indent} is an integer.  Values have the same effect as the
+@code{indent-forms} field in the @code{home-emacs-configuration} record.
+
+Note that indentation rules specified here will subsequently affect all Emacs
+Lisp expressions serialized by the Emacs home service, not just configuration
+for the Emacs packages Home service"
+   (sanitizer (alist-sanitizer indent-forms
+                               ((? symbol?) . (? integer?)))))
+  (extra-init
+   (list-of-elisp-or-gexps '())
+   "A list additional of Elisp expressions or G-expressions to serialize to
+the Emacs user initialization file before all serialized Emacs package
+configuration.  This is useful for code that must set up or configure the
+package serializer itself.")
+  (description
+   (string "")
+   "A brief description of the serializer."))
+
+(define (compose-load-predicates-lambda composer)
+  "Return a lambda that composes multiple load predicates into a single
+s-expression beginning with symbol COMPOSER."
+  (match-lambda
+    (() '())
+    (lst
+     (if (> (length lst) 1)
+         `(,composer ,@lst)
+         (first lst)))))
+
+(define emacs-package-extra-init-file->sexp
+  ;; XXX: Determining on the Elisp side the directory in which to look for
+  ;; extra init files to load is complicated by the fact that variables like
+  ;; 'user-init-file' (and 'server-name') are apparently not defined until
+  ;; after the init file is evaluated.  We use the Elisp variable named by
+  ;; '%emacs-extra-init-files-path-variable', which is set up by the
+  ;; 'home-emacs-packages-service-type'.  An alternative would be to use
+  ;; 'load-path' to store the config directory, but modifying 'load-path' in a
+  ;; way that is not transparent to the user introduces security concerns.
+  (match-lambda
+    ((name . _)
+     `(load (expand-file-name ,name
+                              ,%emacs-extra-init-files-path-variable)
+            #f #f #t))))
+
+(define (emacs-package-hook->normalized-symbol hook)
+  (let* ((str (symbol->string hook)))
+    (string->symbol
+     (if (not (or (string-suffix? "-hook" str)
+                  (string-suffix? "-functions" str)
+                  ;; Rare, e.g., 'emms-metaplaylist-mode-hooks'.
+                  (string-suffix? "-hooks" str)))
+         (string-append str "-hook")
+         str))))
+
+(define (keys-local->simple-sexp keymap package)
+  "Return an Elisp S-expression that configures <emacs-keymap> object KEYMAP
+for package named by the symbol PACKAGE."
+  (match-record keymap <emacs-keymap>
+                (name
+                 package-name
+                 prefix?
+                 repeat?
+                 repeat-exit
+                 repeat-enter
+                 disabled-commands
+                 keys)
+    (let ((keydefs (append (map (match-lambda
+                                  (((? vector? k) . s)
+                                   `(define-key ,name ,k
+                                      ,(elispifiable->elisp s)))
+                                  (((? string? k) . s)
+                                   ;; Function introduced in Emacs 29.1.
+                                   `(keymap-set ,name ,k
+                                                ,(elispifiable->elisp s))))
+                                keys)
+                           (if repeat?
+                               (map (lambda (s)
+                                      `(put ',s 'repeat-map ',name))
+                                    (delete-duplicates
+                                     (append
+                                      (filter-map (match-lambda
+                                                    ((_ . s)
+                                                     (if (or (not s)
+                                                             (memq s repeat-exit))
+                                                         #f
+                                                         s)))
+                                                  keys)
+                                      repeat-enter)
+                                     eq?))
+                               '()))))
+      (append
+       (if prefix?
+           (list `(progn
+                   (defvar-keymap ,name
+                     `:prefix ',name)
+                   ,@keydefs))
+           (list `(if (boundp ',name)
+                      (progn
+                       ,@keydefs)
+                      (with-eval-after-load
+                          ',(or package-name package)
+                        ,@keydefs))))
+       (map (match-lambda
+              ((command . val)
+               `(put ',command 'disabled ,val)))
+            disabled-commands)))))
+
+(define (emacs-package->simple-elisp config)
+  "Return from 'emacs-package' object CONFIG a list containing Elisp
+expressions that configure Emacs using only minimal built-in functionality."
+  (define (load-path->sexp obj)
+    `(add-to-list 'load-path ,obj))
+
+  (define keys-global->sexp
+    (match-lambda  (((? vector? k) . s)
+                    `(global-set-key ,k ,(elispifiable->elisp s)))
+                   (((? string? k) . s)
+                    ;; Function introduced in Emacs 29.1.
+                    `(keymap-global-set ,k ,(elispifiable->elisp s)))))
+
+  (define keys-override->sexp
+    (match-lambda ((k . s)
+                   `(bind-key* ,k ,(elispifiable->elisp s)))))
+
+  (define option->sexp
+    (match-lambda ((key . val)
+                   ;; Macro introduced in Emacs 29.1.
+                   `(setopt ,key ,(elispifiable->elisp val)))))
+
+  (define face->sexp
+    (match-lambda ((face . spec)
+                   `(face-spec-set ',face ',spec))))
+
+  (define hook->sexp
+    (match-lambda ((hook . func)
+                   (let ((hook* (emacs-package-hook->normalized-symbol hook)))
+                     `(add-hook ',hook* (function ,func))))))
+
+  (define auto-mode->sexp
+    (match-lambda ((pat . mode)
+                   `(add-to-list 'auto-mode-alist '(,pat . ,mode)))))
+
+  (define magic-mode->sexp
+    (match-lambda ((pat . mode)
+                   `(add-to-list 'magic-mode-alist '(,pat . ,mode)))))
+
+  (match-record config <emacs-package>
+                (name
+                 extra-init-files
+                 load-force?
+                 load-predicates
+                 load-after-packages
+                 load-paths
+                 autoloads
+                 autoloads-interactive
+                 keys-global
+                 keys-global-keymaps
+                 keys-override
+                 keys-local
+                 options
+                 faces
+                 hooks
+                 auto-modes
+                 magic-modes
+                 extra-after-load
+                 extra-init
+                 extra-keywords)
+
+    (define (autoload->sexp* obj interactive)
+      `(autoload (function ,obj) ,(symbol->string name) #f ,interactive))
+
+    (define autoload->sexp
+      (cut autoload->sexp* <> #f))
+
+    (define autoload-interactive->sexp
+      (cut autoload->sexp* <> #t))
+
+    (define keys-global-keymaps->sexp
+      (match-lambda (((? vector? ks) . obj)
+                     `(progn
+                       (autoload ',obj
+                                 ,(symbol->string name)
+                                 #f #f 'keymap)
+                       (global-set-key ,ks ,obj)))
+                    (((? string? ks) . obj)
+                     `(progn
+                       (autoload ',obj
+                                 ,(symbol->string name)
+                                 #f #f 'keymap)
+                       (keymap-global-set ,ks ,obj)))))
+
+    (define (load-after-packages->sexp load-after extra)
+      (let loop ((load-after (reverse load-after))
+                 (acc '()))
+        (if (null? load-after)
+            acc
+            (loop (cdr load-after)
+                  (cons 'with-eval-after-load
+                        (cons (list 'quote (car load-after))
+                              (if (null? acc)
+                                  extra
+                                  (list acc))))))))
+
+    (let* ((extra-init-files* (map emacs-package-extra-init-file->sexp
+                                   extra-init-files))
+           (load-predicates* (apply (compose-load-predicates-lambda 'and)
+                                    (list load-predicates)))
+           (load-after-packages* load-after-packages)
+           (load-paths* (map load-path->sexp load-paths))
+           (autoloads* (map autoload->sexp autoloads))
+           (autoloads-interactive* (map autoload-interactive->sexp
+                                        autoloads-interactive))
+           (keys-global* (map keys-global->sexp keys-global))
+           (keys-global-keymaps* (map keys-global-keymaps->sexp
+                                      keys-global-keymaps))
+           (keys-override* (map keys-override->sexp keys-override))
+           (keys-local* (append-map (cut keys-local->simple-sexp <> name)
+                                    keys-local))
+           (options* (map option->sexp options))
+           (faces* (map face->sexp faces))
+           (hooks* (map hook->sexp hooks))
+           (auto-modes* (map auto-mode->sexp auto-modes))
+           (magic-modes* (map magic-mode->sexp magic-modes))
+           (extra-after-load* (cond
+                               (load-force?
+                                (list
+                                 `(if (not (require ',name nil t))
+                                      (display-warning
+                                       'initialization
+                                       (format "Failed to load %s" ',name)
+                                       :error)
+                                      ,@extra-after-load)))
+                               ((not (null? extra-after-load))
+                                (list `(with-eval-after-load
+                                           (quote ,name)
+                                         ,@extra-after-load)))
+                               (else '())))
+           (after-packages-sexps (append autoloads*
+                                         autoloads-interactive*
+                                         keys-global*
+                                         keys-override*
+                                         keys-global-keymaps*
+                                         keys-local*
+                                         options*
+                                         faces*
+                                         hooks*
+                                         auto-modes*
+                                         magic-modes*
+                                         extra-after-load*
+                                         extra-init))
+           (combined-sexps (append load-paths*
+                                   extra-init-files*
+                                   (if (null? load-after-packages*)
+                                       after-packages-sexps
+                                       (list (load-after-packages->sexp
+                                              load-after-packages*
+                                              after-packages-sexps)))))
+           (comment-string (string-append ";;; Package "
+                                          (symbol->string name)
+                                          "\n")))
+      (if (null? combined-sexps)
+          '()
+          (append
+           (list (elisp (unelisp-comment comment-string)))
+           (if (null? load-predicates*)
+               (map sexp->elisp combined-sexps)
+               (list (sexp->elisp `(when ,load-predicates*
+                                     ,@combined-sexps)))))))))
+
+(define %emacs-simple-package-serializer
+  (emacs-package-serializer
+   (name 'emacs-simple-package)
+   (procedure emacs-package->simple-elisp)
+   (description "An Emacs package configuration serializer that configures
+Emacs using minimal, built-in Emacs mechanisms, instead of complex macros such
+as @code{use-package}.")))
+
+(define (emacs-package->use-package-elisp config)
+  "Return from 'emacs-package' object CONFIG a list containing Elisp
+expressions that configures Emacs using the 'use-package' macro."
+  (define-syntax unless-null
+    (syntax-rules ()
+      ((_ var exp)
+       (if (null? var)
+           '()
+           exp))
+      ((_ var)
+       var)))
+
+  (define (keys-local->sexp config)
+    (match-record config <emacs-keymap>
+                  (name package-name prefix? repeat? repeat-exit keys)
+      (cond
+       ;; Skip adding a ':bind' keyword when there is a 'package-name',
+       ;; because 'use-package' isn't smart enough to (eval-after-)load it
+       ;; when the keymap is defined in another package.  Also skip adding the
+       ;; keyword when there is a true 'prefix?' field, because 'use-package'
+       ;; (more specifically, 'bind-keys') isn't smart enough to define
+       ;; non-existent keymaps either.
+       ((or package-name prefix? (null? keys)) '())
+       (repeat?
+        (receive (exit rest)
+            (partition (match-lambda
+                         ((_ . binding)
+                          (memq binding repeat-exit)))
+                       keys)
+          `(:repeat-map ,name
+            ,@rest
+            ,@(if (null? exit)
+                  '()
+                  `(:exit
+                    ,(elisp (unelisp-newline))
+                    ,@exit)))))
+       (else `(:map ,name
+               ,@keys)))))
+
+  (define (keys-local->extra-sexps keymap)
+    (match-record keymap <emacs-keymap> (package-name prefix?)
+      (if (or package-name prefix?)
+          (keys-local->simple-sexp keymap
+                                   (emacs-package-name config))
+          '())))
+
+  (define option->sexp
+    (match-lambda ((key . val)
+                   `(,key ,(elispifiable->elisp val)))))
+
+  (define face->sexp
+    (match-lambda ((key . val)
+                   `(,key ,val))))
+
+  (define hook->sexp
+    (match-lambda ((hook . func)
+                   (let ((hook*
+                          (emacs-package-hook->normalized-symbol hook)))
+                     `(,hook* . ,func)))))
+
+  (define use-package-keywords '(#:after
+                                 #:autoload
+                                 #:bind
+                                 #:bind*
+                                 #:bind-keymap
+                                 #:bind-keymap*
+                                 #:catch
+                                 #:commands
+                                 #:config
+                                 #:custom
+                                 #:custom-face
+                                 #:defer
+                                 #:defines
+                                 #:demand
+                                 #:disabled
+                                 #:functions
+                                 #:hook
+                                 #:if
+                                 #:init
+                                 #:interpreter
+                                 #:load
+                                 #:load-path
+                                 #:magic
+                                 #:magic-fallback
+                                 #:mode
+                                 #:no-require
+                                 #:preface
+                                 #:requires
+                                 #:unless
+                                 #:when))
+
+  (define symbol->keyword*
+    (match-lambda
+      ((? symbol? kw)
+       (let* ((str (symbol->string kw))
+              (str* (if (string-prefix? ":" str)
+                        (string-drop str 1)
+                        str)))
+         (symbol->keyword (string->symbol str*))))
+      ((? keyword? kw)
+       kw)))
+
+  (define (use-package-keyword? obj)
+    (memq (symbol->keyword* obj) use-package-keywords))
+
+  (define extra-keyword->sexp
+    (match-lambda
+      (((? use-package-keyword? kw) . exps)
+       `(,(symbol->keyword* kw) ,@exps))
+      (_ #f)))
+
+  (match-record config <emacs-package>
+                (name
+                 extra-init-files
+                 load-force?
+                 load-predicates
+                 load-after-packages
+                 load-paths
+                 autoloads
+                 autoloads-interactive
+                 keys-global
+                 keys-global-keymaps
+                 keys-override
+                 keys-local
+                 options
+                 faces
+                 hooks
+                 auto-modes
+                 magic-modes
+                 extra-after-load
+                 extra-init
+                 extra-keywords)
+    (let* ((extra-init-files* (map emacs-package-extra-init-file->sexp
+                                   extra-init-files))
+           (load-predicates* (apply (compose-load-predicates-lambda 'and)
+                                    (list load-predicates)))
+           (load-after-packages* load-after-packages)
+           (autoloads* autoloads)
+           (autoloads-interactive* autoloads-interactive)
+           (load-paths* load-paths)
+           (keys-global+local (append keys-global
+                                      (append-map keys-local->sexp
+                                                  keys-local)))
+           (keys-global-keymaps* keys-global-keymaps)
+           (keys-override* keys-override)
+           (options* (map option->sexp options))
+           (faces* (map face->sexp faces))
+           (hooks* (map hook->sexp hooks))
+           (auto-modes* auto-modes)
+           (magic-modes* magic-modes)
+           (extra-after-load* extra-after-load)
+           (extra-init* (append extra-init
+                                (append-map keys-local->extra-sexps
+                                            keys-local)))
+           (extra-keywords* (apply append (filter-map extra-keyword->sexp
+                                                      extra-keywords)))
+           (comment-string (string-append ";;; Package "
+                                          (symbol->string name)
+                                          "\n"))
+           (combined-sexps (append
+                            ;; XXX: Load 'extra-init-files' before the
+                            ;; 'use-package' macro, so that variables defined
+                            ;; in the loaded files can be used in the
+                            ;; 'options' field.  If we put 'load' calls in the
+                            ;; 'use-package' ':init' keyword, any variables
+                            ;; from extra files referenced in 'options' will
+                            ;; not be bound.  This is because 'use-package'
+                            ;; sets variables from the ':custom' keyword
+                            ;; before evaluating ':init' s-expressions.
+                            extra-init-files*
+                            (list
+                             `(use-package
+                               ,name
+                               ,@(if load-force?
+                                     '(:demand t)
+                                     '(:defer t))
+                               ,@(unless-null load-after-packages*
+                                              `(:after ,load-after-packages*))
+                               ,@(unless-null load-paths*
+                                              `(:load-path ,load-paths*))
+                               ,@(unless-null autoloads*
+                                              `(:autoload ,autoloads*))
+                               ,@(unless-null autoloads-interactive*
+                                              `(:commands
+                                                ,autoloads-interactive*))
+                               ,@(unless-null keys-global+local
+                                              `(:bind ,keys-global+local))
+                               ,@(unless-null keys-override*
+                                              `(#:bind* ,keys-override*))
+                               ,@(unless-null keys-global-keymaps*
+                                              `(:bind-keymap
+                                                ,keys-global-keymaps*))
+                               ,@(unless-null hooks*
+                                              `(:hook ,hooks*))
+                               ,@(unless-null auto-modes*
+                                              `(:mode ,auto-modes*))
+                               ,@(unless-null magic-modes*
+                                              `(:magic ,magic-modes*))
+                               ,@(unless-null faces*
+                                              `(:custom-face
+                                                ,@(append (list
+                                                           (elisp
+                                                            (unelisp-newline)))
+                                                          faces*)))
+                               ,@(unless-null options*
+                                              `(:custom
+                                                ,@(append (list
+                                                           (elisp
+                                                            (unelisp-newline)))
+                                                          options*)))
+                               ,@(unless-null extra-after-load*
+                                              `(:config
+                                                ,@(append (list
+                                                           (elisp
+                                                            (unelisp-newline)))
+                                                          extra-after-load*)))
+                               ,@extra-keywords*
+                               ,@(unless-null extra-init*
+                                              `(:init
+                                                ,@(append (list
+                                                           (elisp
+                                                            (unelisp-newline)))
+                                                          extra-init*))))))))
+      (if (null? combined-sexps)
+          '()
+          (append
+           (list (elisp (unelisp-comment comment-string)))
+           (if (null? load-predicates*)
+               (map sexp->elisp combined-sexps)
+               (list (sexp->elisp `(when ,load-predicates*
+                                     ,@combined-sexps)))))))))
+
+(define %emacs-use-package-serializer
+  (emacs-package-serializer
+   (name 'emacs-use-package)
+   (procedure emacs-package->use-package-elisp)
+   ;; 'use-package' built-in to Emacs >=29.1.
+   (dependencies (list emacs-use-package))
+   (indent-forms '((use-package . 1)))
+   (extra-init
+    (list
+     (elisp (setq use-package-hook-name-suffix nil))))
+   (description "An Emacs package configuration serializer that configures
+Emacs with the @code{use-package} macro.")))
+
+(define-configuration/no-serialization home-emacs-packages-configuration
+  (package-serializer
+   (emacs-package-serializer %emacs-simple-package-serializer)
+   "The serializer to use for configuration specified by @code{emacs-package}
+objects.")
+  (packages
+   (list-of-emacs-packages '())
+   "A list of @code{emacs-package} objects containing configuration for Emacs
+packages."))
+
+(define* (merge-records original extensions accessor proc
+                        #:key (type? (const #t)) (= equal?))
+  "Extend list of records ORIGINAL with lists of records EXTENSIONS by merging
+all records where the field values of accessed by ACCESSOR are equal according
+to equality predicate = using PROC, a procedure that takes a record as its
+first argument and a list of records as its second argument and should return
+a single record object.  All objects that do not satisfy type predicate TYPE?
+are added to the returned list without comparison."
+  (let loop ((lst (apply append original extensions))
+             (acc '()))
+    (cond
+     ((null? lst) (reverse acc))
+     ((not (type? (car lst)))
+      (loop (cdr lst)
+            (cons (car lst) acc)))
+     ((partition
+       (lambda (ext)
+         (and (type? ext)
+              (= (apply accessor (list ext))
+                 (apply accessor (list (car lst))))))
+       (cdr lst))
+      (lambda (matches rest) (not (null? matches)))
+      => (lambda (matches rest)
+           (loop rest
+                 (cons (apply proc (car lst) (list matches))
+                       acc))))
+     (else (loop (cdr lst)
+                 (cons (car lst) acc))))))
+
+(define* (extend-default original extensions
+                         default field
+                         #:key (= eq?))
+  "Extend the value of ORIGINAL with any value in the list of EXTENSIONS that
+is not equal to the value of DEFAULT, signaling an error if there is any value
+in EXTENSIONS that is not equal to either ORIGINAL or DEFAULT according to
+equality predicate =, which defaults conservatively to 'eq?'.  For example, if
+DEFAULT and ORIGINAL are both #f, and at least one element of EXTENSIONS is
+#t, return #t, but if DEFAULT is 'foo', ORIGINAL is 'bar', and EXTENSIONS
+contains a value 'baz', then signal an error."
+  (let* ((new (fold (lambda (elem ret)
+                      (cond
+                       ((= elem original) ret)
+                       ((= elem default) elem)
+                       (else (configuration-field-error
+                              #f field elem))))
+                    '()
+                    extensions)))
+    (if (null? new) original new)))
+
+(define (extend-emacs-keymap orig exts)
+  "Extend <emacs-keymap> object ORIG with list of <emacs-keymap> objects
+EXTS."
+  (define %default-keymap (emacs-keymap))
+
+  (match-record orig <emacs-keymap>
+                (package-name
+                 repeat?
+                 repeat-exit
+                 repeat-enter
+                 disabled-commands
+                 keys)
+    (emacs-keymap
+     (inherit orig)
+     (package-name (extend-default package-name
+                                   (map emacs-keymap-package-name exts)
+                                   (emacs-keymap-package-name %default-keymap)
+                                   'package-name))
+     (repeat? (extend-default repeat?
+                              (map emacs-keymap-repeat? exts)
+                              (emacs-keymap-repeat? %default-keymap)
+                              'repeat?))
+     (repeat-exit (delete-duplicates (apply append
+                                            repeat-exit
+                                            (map emacs-keymap-repeat-exit exts))
+                                     equal?))
+     (repeat-enter (delete-duplicates (apply append
+                                             repeat-enter
+                                             (map emacs-keymap-repeat-enter exts))
+                                      equal?))
+     (disabled-commands (apply append
+                               disabled-commands
+                               (map emacs-keymap-disabled-commands exts)))
+     (keys (apply append keys (map emacs-keymap-keys exts))))))
+
+(define (merge-emacs-keymaps keymaps . rest)
+  "Merge list of <emacs-keymap> objects KEYMAPS with lists of <emacs-keymap>
+objects in REST."
+  (merge-records keymaps rest emacs-keymap-name extend-emacs-keymap))
+
+(define (extend-emacs-package orig exts)
+  "Extend <emacs-package> object ORIG with list of <emacs-package> objects
+EXTS."
+  (define %default-package (emacs-package))
+
+  (match-record orig <emacs-package>
+                (package
+                  extra-packages
+                  install?
+                  extra-init-files
+                  user-files
+                  load-force?
+                  load-predicates
+                  load-after-packages
+                  load-paths
+                  autoloads
+                  autoloads-interactive
+                  keys-global
+                  keys-global-keymaps
+                  keys-override
+                  keys-local
+                  options
+                  faces
+                  hooks
+                  auto-modes
+                  magic-modes
+                  extra-after-load
+                  extra-init
+                  extra-keywords)
+    (emacs-package
+      (inherit orig)
+      (package (extend-default package
+                               (map emacs-package-package exts)
+                               (emacs-package-package %default-package)
+                               'package))
+      (extra-packages (apply append
+                             extra-packages
+                             (map emacs-package-extra-packages exts)))
+      (install? (extend-default install?
+                                (map emacs-package-install? exts)
+                                (emacs-package-install? %default-package)
+                                'install?))
+      (extra-init-files (merge-alists extra-init-files
+                                      (append-map emacs-package-extra-init-files
+                                                  exts)))
+      (user-files (merge-alists user-files
+                                (append-map emacs-package-user-files
+                                            exts)))
+      (load-force? (extend-default load-force?
+                                   (map emacs-package-load-force? exts)
+                                   (emacs-package-load-force? %default-package)
+                                   'load-force?))
+      (load-predicates (apply append
+                              load-predicates
+                              (map emacs-package-load-predicates exts)))
+      (load-after-packages (delete-duplicates
+                            (apply append
+                                   load-predicates
+                                   (map emacs-package-load-predicates exts))
+                            eq?))
+      (load-paths (delete-duplicates
+                   (apply append
+                          load-paths
+                          (map emacs-package-load-paths exts))
+                   equal?))
+      (autoloads (delete-duplicates
+                  (apply append
+                         autoloads
+                         (map emacs-package-autoloads exts))
+                  eq?))
+      (autoloads-interactive (delete-duplicates
+                              (apply append
+                                     autoloads-interactive
+                                     (map emacs-package-autoloads-interactive exts))
+                              eq?))
+      (keys-global (apply append
+                          keys-global
+                          (map emacs-package-keys-global exts)))
+      (keys-global-keymaps (apply append
+                                  keys-global-keymaps
+                                  (map emacs-package-keys-global-keymaps exts)))
+      (keys-override (apply append
+                            keys-override
+                            (map emacs-package-keys-override exts)))
+      (keys-local (merge-records keys-local
+                                 (map emacs-package-keys-local exts)
+                                 emacs-keymap-name
+                                 extend-emacs-keymap))
+      (options (apply append options (map emacs-package-options exts)))
+      (faces (apply append faces (map emacs-package-faces exts)))
+      (hooks (apply append hooks (map emacs-package-hooks exts)))
+      (auto-modes (apply append
+                         auto-modes
+                         (map emacs-package-auto-modes exts)))
+      (magic-modes (apply append
+                          magic-modes
+                          (map emacs-package-magic-modes exts)))
+      (extra-after-load (apply append
+                               extra-after-load
+                               (map emacs-package-extra-after-load exts)))
+      (extra-init (apply append
+                         extra-init
+                         (map emacs-package-extra-init exts)))
+      (extra-keywords (merge-alists extra-keywords
+                                    (append-map emacs-package-extra-keywords
+                                                exts))))))
+
+(define (merge-emacs-packages packages . rest)
+  "Merge list of <emacs-package> objects PACKAGES with lists of
+<emacs-package> objects in REST.  Records with a non-null 'load-predicates'
+field will not be merged, which is useful, for example, when there are
+multiple different configurations for a package whose load predicates test
+conditions that can only be known at Emacs initialization time."
+  (define (emacs-package-no-predicates? config)
+    (match-record config <emacs-package>
+                  (load-predicates)
+      (null? load-predicates)))
+
+  (merge-records packages rest emacs-package-name extend-emacs-package
+                 #:type? emacs-package-no-predicates?))
+
+(define (emacs-packages->elisp packages serializer)
+  "Return a list of Elisp expressions containing the configuration for the
+list of <emacs-package> objects PACKAGES serialized according to SERIALIZER,
+an <emacs-package-serializer> object."
+
+  (define extra-init-files-load-dir-sexps
+    ;; Set an Elisp variable so that package serializers can use it to find
+    ;; extra init files.
+    (list `(setq ,%emacs-extra-init-files-path-variable
+                 ,%emacs-extra-init-files-path)))
+
+  (append extra-init-files-load-dir-sexps
+          (list (elisp (unelisp-newline)))
+          (emacs-package-serializer-extra-init serializer)
+          (list (elisp (unelisp-newline)))
+          ;; Multiple packages with the same name should already have been
+          ;; merged by 'home-emacs-packages-extensions'.
+          (apply append
+                 (interpose
+                  (map (emacs-package-serializer-procedure serializer)
+                       packages)
+                  (list (elisp (unelisp-newline)))
+                  'suffix))))
+
+(define (home-emacs-packages-configuration-extension config)
+  "Return a 'home-emacs-extension' from 'home-emacs-packages-configuration'
+CONFIG."
+  (define (emacs-package->installable-packages config)
+    (match-record config <emacs-package>
+                  (package extra-packages install?)
+      (if install?
+          (append (if (null? package)
+                      '()
+                      (list package))
+                  extra-packages)
+          '())))
+
+  (match-record config <home-emacs-packages-configuration>
+                (package-serializer
+                 packages)
+    ;; XXX: Store the old value of PACKAGES because the newly defined
+    ;; 'packages' field will overshadow its value when we try to reference it
+    ;; in the 'init-files' and 'user-files' field declarations.
+    (let ((packages* packages))
+      (home-emacs-extension
+       (packages (delete-duplicates
+                  (append
+                   (emacs-package-serializer-dependencies package-serializer)
+                   (append-map emacs-package->installable-packages packages))
+                  eq?))
+       (indent-forms (emacs-package-serializer-indent-forms package-serializer))
+       (init-file (emacs-packages->elisp packages* package-serializer))
+       ;; XXX: For user file entries with names matching those already
+       ;; declared for the home Emacs service type, their contents will be
+       ;; appended to the already existing entries.
+       (user-files (merge-alists (append-map emacs-package-user-files
+                                             packages*)))))))
+
+(define (home-emacs-packages-extra-files config)
+  "Return from <home-emacs-packages-configuration> CONFIG an association list
+of extra initialization files to create in XDG_CONFIG_HOME."
+  (match-record config <home-emacs-packages-configuration>
+                (packages)
+    (let ((created-files `(,%emacs-early-init-filename
+                           ,%emacs-user-init-filename)))
+      (append-map
+       (lambda (package)
+         (append-map (match-lambda
+                       ((name . files)
+                        ;; XXX: Some packages could still try to create files
+                        ;; with the same name.
+                        (when (member name created-files)
+                          (configuration-field-error #f 'extra-init-files
+                                                     (cons name files)))
+                        (set! created-files (cons name created-files))
+                        (list
+                         (list (emacs-config-filename name)
+                               (apply composite-file
+                                      (basename name)
+                                      (ensure-list files)))))
+                       (_ '()))
+                     (emacs-package-extra-init-files package)))
+       packages))))
+
+(define (home-emacs-packages-extensions original extension-packages)
+  "Extend the Emacs packages home service configuration ORIGINAL with list of
+extension packages EXTENSION-PACKAGES."
+  (match-record original <home-emacs-packages-configuration>
+                (packages)
+    (home-emacs-packages-configuration
+     (inherit original)
+     (packages (merge-emacs-packages (append packages extension-packages))))))
+
+(define home-emacs-packages-service-type
+  (service-type (name 'home-emacs-packages-service)
+                (extensions
+                 (list (service-extension
+                        home-emacs-service-type
+                        home-emacs-packages-configuration-extension)
+                       (service-extension
+                        home-xdg-configuration-files-service-type
+                        home-emacs-packages-extra-files)))
+                (default-value '())
+                (compose concatenate)
+                (extend home-emacs-packages-extensions)
+                (description
+                 "Configure packages for the GNU Emacs extensible text editor.")))
+
+
+;;;
+;;; Utility functions
+;;;
+
+(define (schemified-elisp->home-emacs-packages-configuration lst)
+  "Convert LST, a list of s-expressions, into Emacs configuration records.
+Returns a `home-emacs-packages-configuration' record. "
+  (define elisp->scheme
+    (match-lambda
+      ('t
+       #t)
+      ('nil
+       #f)
+      ((? constant? obj)
+       obj)
+      (('quote obj)
+       obj)
+      (obj
+       (sexp->elisp obj))))
+
+  (define (variable-specs->alist specs)
+    (let lp ((specs specs)
+             (acc '()))
+      (match specs
+        (()
+         (reverse! acc))
+        (((? blank?) . rest)
+         (lp rest acc))
+        ((var val . rest)
+         (lp rest (cons (cons var (elisp->scheme val))
+                        acc)))
+        (_
+         (raise (formatted-message (G_ "invalid `setq'/`setopt' in file")))))))
+
+  (define (use-package->emacs-package name body)
+    (define elisp-keyword?
+      (match-lambda
+        ((? symbol? obj)
+         (string-prefix? ":" (symbol->string obj)))
+        (_ #f)))
+
+    (define dotted-pair?
+      (match-lambda
+        ((head . (not (? pair?)))
+         #t)
+        (_ #f)))
+
+    (define list-of-dotted-pairs?
+      (list-of dotted-pair?))
+
+    (let lp ((lst body)
+             (package (emacs-package (name name))))
+      (match lst
+        (()
+         package)
+        (((? elisp-keyword? kw) . rest)
+         (receive (args rest)
+             (break elisp-keyword? rest)
+           (match kw
+             (':demand
+              (lp rest
+                  (emacs-package
+                    (inherit package)
+                    (load-force? (match (remove blank? args)
+                                   ('nil #f)
+                                   (_ #t))))))
+             (':defer
+              ;; 'load-force?' is false by default, and here we only set it to
+              ;; true when there is a non-nil ':demand' keyword.
+              (lp rest package))
+             ((or ':if ':when)
+              (lp rest
+                  (emacs-package
+                    (inherit package)
+                    (load-predicates (append (emacs-package-load-predicates
+                                              package)
+                                             (match (remove blank? args)
+                                               ((exp)
+                                                (list (elisp (unelisp exp))))
+                                               (_ '())))))))
+             (':unless
+              (lp rest
+                  (emacs-package
+                    (inherit package)
+                    (load-predicates (append (emacs-package-load-predicates
+                                              package)
+                                             (match (remove blank? args)
+                                               ((exp)
+                                                (list (elisp (not
+                                                              (unelisp exp)))))
+                                               (_ '())))))))
+             (':after
+              (lp rest
+                  (emacs-package
+                    (inherit package)
+                    (load-after-packages (append
+                                          (emacs-package-load-after-packages
+                                           package)
+                                          (match (remove blank? args)
+                                            (((':all
+                                               . (? list-of-symbols? lst)))
+                                             lst)
+                                            (((':any . rest))
+                                             ;; Ignore, because we can't
+                                             ;; guarantee equivalent behavior.
+                                             '())
+                                            ((? list-of-symbols? lst)
+                                             lst)
+                                            (((? list-of-symbols? lst))
+                                             lst)
+                                            (_ '())))))))
+             (':load-path
+              (lp rest
+                  (emacs-package
+                    (inherit package)
+                    (load-paths (append (emacs-package-load-paths package)
+                                        (filter string?
+                                                (match args
+                                                  (((? list? lst))
+                                                   lst)
+                                                  ((? list? lst)
+                                                   lst)
+                                                  (_ '()))))))))
+             (':autoload
+              (lp rest
+                  (emacs-package
+                    (inherit package)
+                    (autoloads (append (emacs-package-autoloads package)
+                                       (match (remove blank? args)
+                                         ((? list-of-symbols? lst)
+                                          lst)
+                                         (((? list-of-symbols? lst))
+                                          lst)
+                                         (_ '())))))))
+             (':commands
+              (lp rest
+                  (emacs-package
+                    (inherit package)
+                    (autoloads-interactive (append
+                                            (emacs-package-autoloads-interactive
+                                             package)
+                                            (match (remove blank? args)
+                                              ((? list-of-symbols? lst)
+                                               lst)
+                                              (((? list-of-symbols? lst))
+                                               lst)
+                                              (_ '())))))))
+             (':bind*
+              (lp rest
+                  (emacs-package
+                    (inherit package)
+                    (keys-override
+                     (append (emacs-package-keys-override package)
+                             (filter dotted-pair?
+                                     (match args
+                                       (((? list? lst))
+                                        lst)
+                                       (_ args))))))))
+             (':bind
+              (receive (global local)
+                  (break elisp-keyword? (match args
+                                          (((? list? lst))
+                                           lst)
+                                          (_ args)))
+                (lp rest
+                    (emacs-package
+                      (inherit package)
+                      (keys-global (append (emacs-package-keys-global package)
+                                           (filter dotted-pair? global)))
+                      (keys-local
+                       (append
+                        (emacs-package-keys-local package)
+                        (let lp/inner ((lst (remove blank? local))
+                                       (keymaps '()))
+                          (match lst
+                            ((':map (? symbol? kmap) . rest)
+                             (receive (kspecs rest)
+                                 (break (cut memq <> '(:map :repeat-map))
+                                        rest)
+                               (lp/inner rest
+                                         (append
+                                          keymaps
+                                          (list (emacs-keymap
+                                                 (name kmap)
+                                                 (keys (filter dotted-pair?
+                                                               kspecs))))))))
+                            ((':repeat-map (? symbol? kmap) . rest)
+                             (receive (kspecs rest)
+                                 (break (cut memq <> '(:map :repeat-map))
+                                        rest)
+                               (lp/inner rest
+                                         (append
+                                          keymaps
+                                          (list
+                                           (emacs-keymap
+                                            (name kmap)
+                                            (repeat? #t)
+                                            (repeat-exit
+                                             (filter-map
+                                              (match-lambda
+                                                (((? string-or-vector?)
+                                                  . (? symbol? sym))
+                                                 sym)
+                                                (_ #f))
+                                              (take-while
+                                               (negate (cut eq? <>
+                                                            ':continue))
+                                               (drop-while
+                                                (negate (cut eq? <>
+                                                             ':exit))
+                                                kspecs))))
+                                            (keys (filter dotted-pair?
+                                                          kspecs))))))))
+                            (_ keymaps)))))))))
+             (':bind-keymap
+              (lp rest
+                  (emacs-package
+                    (inherit package)
+                    (keys-global-keymaps (append
+                                          (emacs-package-keys-global-keymaps
+                                           package)
+                                          (filter dotted-pair?
+                                                  (match args
+                                                    (((? list? lst))
+                                                     lst)
+                                                    (_ args))))))))
+             (':custom
+              (lp rest
+                  (emacs-package
+                    (inherit package)
+                    (options (append (emacs-package-options package)
+                                     (filter-map
+                                      (match-lambda
+                                        ((var val . rest)
+                                         `(,var . ,(elisp->scheme val)))
+                                        (_ #f))
+                                      (match args
+                                        (((and ((? list?) . rest) lst))
+                                         ;; E.g., :custom ((foo bar))
+                                         lst)
+                                        (_ args))))))))
+             (':custom-face
+              (lp rest
+                  (emacs-package
+                    (inherit package)
+                    (faces (append (emacs-package-faces package)
+                                   (filter-map (match-lambda
+                                                 (((? symbol? face)
+                                                   ((? pair? spec) ..1))
+                                                  `(,face . ,spec))
+                                                 (_ #f))
+                                               args))))))
+             (':hook
+              (lp rest
+                  (emacs-package
+                    (inherit package)
+                    (hooks (append (emacs-package-hooks package)
+                                   (match args
+                                     ((((? list-of-symbols? hooks)
+                                        . (? symbol? func)))
+                                      (map (cut cons <> func)
+                                           hooks))
+                                     (((? list-of-dotted-pairs? lst))
+                                      (filter-map (match-lambda
+                                                    (((? symbol? hook)
+                                                      . (? symbol? func))
+                                                     (cons hook func))
+                                                    (_ #f))
+                                                  lst))
+                                     ((or (? list-of-symbols? hooks)
+                                          ((? list-of-symbols? hooks)))
+                                      (map
+                                       (cute cons <>
+                                             (symbol-append name '-mode))
+                                       hooks))
+                                     (_ '())))))))
+             (':mode
+              (lp rest
+                  (emacs-package
+                    (inherit package)
+                    (auto-modes (append (emacs-package-auto-modes package)
+                                        (match args
+                                          ((or ((? string? strings) ..1)
+                                               (((? string? strings) ..1)))
+                                           (map (cut cons <> name)
+                                                strings))
+                                          ((or ((? list-of-dotted-pairs? lst))
+                                               (? list-of-dotted-pairs? lst))
+                                           (filter
+                                            (match-lambda
+                                              (((? string?) . (? symbol?))
+                                               #t)
+                                              (_ #f))
+                                            lst))
+                                          (_ '())))))))
+             (':magic
+              (lp rest
+                  (emacs-package
+                    (inherit package)
+                    (magic-modes (append (emacs-package-magic-modes package)
+                                         (match args
+                                           ((or ((? string? strings) ..1)
+                                                (((? string? strings) ..1)))
+                                            (map (cut cons <> name)
+                                                 strings))
+                                           ((or ((? list-of-dotted-pairs? lst))
+                                                (? list-of-dotted-pairs? lst))
+                                            (filter
+                                             (match-lambda
+                                               (((? string?) . (? symbol?))
+                                                #t)
+                                               (_ #f))
+                                             lst))
+                                           (_ '())))))))
+             (':config
+              (lp rest
+                  (emacs-package
+                    (inherit package)
+                    (extra-after-load (append (emacs-package-extra-after-load
+                                               package)
+                                              (map sexp->elisp
+                                                   args))))))
+             (':init
+              (lp rest
+                  (emacs-package
+                    (inherit package)
+                    (extra-init (append (emacs-package-extra-init package)
+                                        (map sexp->elisp
+                                             args))))))
+             (kw
+              (lp rest
+                  (emacs-package
+                    (inherit package)
+                    (extra-keywords (append (emacs-package-extra-keywords
+                                             package)
+                                            (list
+                                             `(,kw . ,(map sexp->elisp
+                                                           args)))))))))))
+        (((? blank?) . rest)
+         (lp rest package))
+        (_ (raise (formatted-message
+                   (G_ "invalid `use-package' form in file")))))))
+
+  (let loop ((lst lst)
+             (sexps '())
+             (config (emacs-package
+                       (name 'emacs)))
+             (packages '()))
+    (match lst
+      (()
+       (home-emacs-packages-configuration
+        (packages (merge-emacs-packages
+                   (append
+                    (list
+                     (emacs-package
+                       (inherit config)
+                       (keys-local
+                        (merge-emacs-keymaps
+                         (emacs-package-keys-local config)))
+                       (extra-init
+                        (append
+                         (emacs-package-extra-init config)
+                         (map sexp->elisp (reverse sexps))))))
+                    (reverse packages))))))
+      ((((or 'setq 'setopt) . specs) . rest)
+       (loop rest
+             sexps
+             (emacs-package
+               (inherit config)
+               (options (append (emacs-package-options config)
+                                (variable-specs->alist specs))))
+             packages))
+      ((`(bind-key* ,(? string-or-vector? key)
+                    (,(or 'quote 'function) ,(? symbol? def)) . ,_)
+        . rest)
+       (loop rest
+             sexps
+             (emacs-package
+               (inherit config)
+               (keys-override (append (emacs-package-keys-override config)
+                                      (list (cons key def)))))
+             packages))
+      (((or `(global-set-key ,(? vector? key)
+                             (,(or 'quote 'function) ,(? symbol? def)))
+            `(global-set-key (kbd ,(? string? key))
+                             (,(or 'quote 'function) ,(? symbol? def)))
+            `(keymap-global-set ,(? string? key)
+                                (,(or 'quote 'function) ,(? symbol? def)))
+            `(bind-key ,(? string-or-vector? key)
+                       (,(or 'quote 'function) ,(? symbol? def)))
+            `(bind-key ,(? string-or-vector? key)
+                       (,(or 'quote 'function) ,(? symbol? def))
+                       ,(or `(quote global-map)
+                            'global-map)
+                       . ,_))
+        . rest)
+       (loop rest
+             sexps
+             (emacs-package
+               (inherit config)
+               (keys-global (append (emacs-package-keys-global config)
+                                    (list (cons key def)))))
+             packages))
+      (((or `(define-key ,(? symbol? kmap)
+               ,(? vector? key)
+               (,(or 'quote 'function) ,(? symbol? def)))
+            `(define-key ,(? symbol? kmap)
+               (kbd ,(? string? key))
+               (,(or 'quote 'function) ,(? symbol? def)))
+            `(keymap-set ,(? symbol? kmap)
+                         ,(? string? key)
+                         (,(or 'quote 'function) ,(? symbol? def)))
+            `(bind-key ,(? string-or-vector? key)
+                       (,(or 'quote 'function) ,(? symbol? def))
+                       ,(or `(quote ,kmap) kmap)
+                       . ,_))
+        . rest)
+       (loop rest
+             sexps
+             (emacs-package
+               (inherit config)
+               (keys-local (append (emacs-package-keys-local config)
+                                   (list (emacs-keymap
+                                          (name kmap)
+                                          (keys
+                                           (list (cons key def))))))))
+             packages))
+      ((`(add-hook (quote ,hook) (quote ,fun)) . rest)
+       (loop rest
+             sexps
+             (emacs-package
+               (inherit config)
+               (hooks (append (emacs-package-hooks config)
+                              (cons hook fun))))
+             packages))
+      (((or `(defface ,face
+               ,(and (((or 'default #t 't (? list?)) . (prop . rest))
+                      ..1)
+                     spec))
+            `(face-spec-set (quote ,face)
+                            ,(and (((or 'default #t 't (? list?))
+                                    . (prop . rest))
+                                   ..1)
+                                  spec)))
+        . rest)
+       (loop rest
+             sexps
+             (emacs-package
+               (inherit config)
+               (faces (append (emacs-package-faces config)
+                              (cons face spec))))
+             packages))
+      ((`(use-package ,(? symbol? package) . ,body) . rest)
+       (loop rest
+             sexps
+             config
+             (cons* (use-package->emacs-package package body)
+                    packages)))
+      (((? blank?) . rest)
+       (loop rest
+             sexps
+             config
+             packages))
+      ((exp . rest)
+       (loop rest
+             (cons (sexp->elisp exp) sexps)
+             config
+             packages)))))
+
+(define* (elisp->code exp #:optional sexp)
+  "Return Elisp EXP with blanks removed. When SEXP is non-nil, do not wrap in
+an 'elisp' form."
+  ;; Simple serialization for Elisp expressions containing no G-expressions or
+  ;; file-likes.
+  (let ((exp (fold-right/elisp (lambda (t s)
+                                 (match t
+                                   ((? vertical-space?)
+                                    '(unelisp-newline))
+                                   ((? page-break?)
+                                    '(unelisp-page-break))
+                                   ((? comment?)
+                                    `(unelisp-comment
+                                      ,(comment->string t)))
+                                   (_ t)))
+                               (lambda (t s)
+                                 (if (not t)
+                                     (list->dotted-list s)
+                                     s))
+                               cons
+                               '()
+                               exp)))
+    (if sexp
+        exp
+        `(elisp ,exp))))
+
+(define (home-emacs-packages-configuration->code config)
+  "Return a Scheme s-expression creating a 'home-emacs-packages-configuration'
+record equivalent to CONFIG."
+  (define-syntax unless-null
+    (syntax-rules ()
+      ((_ var exp)
+       (if (null? var)
+           '()
+           (list (list 'var exp))))))
+
+  (define (alist->code lst)
+    (list (if (any (match-lambda
+                     ((var . (? elisp? val))
+                      #t)
+                     (_ #f))
+                   lst)
+              'quasiquote
+              'quote)
+          (map (match-lambda
+                 ((var . (? elisp? val))
+                  ;; Works because `quasiquote' expands `unquote' forms like
+                  ;; `(a . ,C) correctly into (a . C), and
+                  ;; `pretty-print-elisp' prints them nicely.
+                  (cons var (list 'unquote
+                                  (elisp->code val))))
+                 ((var . val)
+                  (cons var val)))
+               lst)))
+
+  (define (emacs-keymap->code config)
+    (match-record config <emacs-keymap>
+                  (name
+                   repeat?
+                   repeat-exit
+                   repeat-enter
+                   disabled-commands
+                   keys)
+      `(emacs-keymap
+        (name (quote ,name))
+        ,@(if (not repeat?)
+              '()
+              (list `(repeat? ,repeat?)))
+        ,@(unless-null repeat-exit
+                       `(quote ,repeat-exit))
+        ,@(unless-null repeat-enter
+                       `(quote ,repeat-enter))
+        ,@(unless-null disabled-commands
+                       `(quote ,disabled-commands))
+        ,@(unless-null keys
+                       `(quote ,keys)))))
+
+  (define (emacs-package->code config)
+    (match-record config <emacs-package>
+                  (name
+                   load-force?
+                   load-predicates
+                   load-after-packages
+                   load-paths
+                   autoloads
+                   autoloads-interactive
+                   keys-global
+                   keys-global-keymaps
+                   keys-override
+                   keys-local
+                   options
+                   faces
+                   hooks
+                   auto-modes
+                   magic-modes
+                   extra-after-load
+                   extra-init
+                   extra-keywords)
+      `(emacs-package
+         (name (quote ,name))
+         ,@(if (not load-force?)
+               '()
+               (list `(load-force? ,load-force?)))
+         ,@(unless-null load-predicates
+                        `(list ,@(map elisp->code
+                                      load-predicates)))
+         ,@(unless-null load-after-packages
+                        `(quote ,load-after-packages))
+         ,@(unless-null load-paths
+                        `(quote ,(filter string?
+                                         load-paths)))
+         ,@(unless-null autoloads
+                        `(quote ,autoloads))
+         ,@(unless-null autoloads-interactive
+                        `(quote ,autoloads-interactive))
+         ,@(unless-null keys-global
+                        `(quote ,keys-global))
+         ,@(unless-null keys-global-keymaps
+                        `(quote ,keys-global-keymaps))
+         ,@(unless-null keys-override
+                        `(quote ,keys-override))
+         ,@(unless-null keys-local
+                        `(list ,@(map emacs-keymap->code
+                                      keys-local)))
+         ,@(unless-null options
+                        (alist->code options))
+         ,@(unless-null faces
+                        `(quote ,faces))
+         ,@(unless-null hooks
+                        `(quote ,hooks))
+         ,@(unless-null auto-modes
+                        `(quote ,auto-modes))
+         ,@(unless-null magic-modes
+                        `(quote ,magic-modes))
+         ,@(unless-null extra-after-load
+                        `(elisp* ,@(map (cut elisp->code <> #t)
+                                        extra-after-load)))
+         ,@(unless-null extra-init
+                        `(elisp* ,@(map (cut elisp->code <> #t)
+                                        extra-init)))
+         ,@(unless-null extra-keywords
+                        (list 'quasiquote
+                              (map (match-lambda
+                                     ((head . tail)
+                                      `(,head
+                                        ,@(map (lambda (e)
+                                                 (list 'unquote
+                                                       (elisp->code e)))
+                                               tail))))
+                                   extra-keywords))))))
+
+  (match-record config <home-emacs-packages-configuration>
+                (packages)
+    `(home-emacs-packages-configuration
+      (packages
+       ,(if (null? packages)
+            '(quote ())
+            `(list ,@(map emacs-package->code
+                          packages)))))))
+
+(define (read-elisp* port)
+  "Read Elisp from port."
+  ;; 'read-elisp' from (language elisp parser) supports the following
+  ;; character read syntaxes:
+  ;;
+  ;; ?X (supported), ?\uXXXX (supported), ?\uXXXXXXXX, (supported), ?\X
+  ;; (supported), ?\XXX (octal, supported)
+  ;;
+  ;; 'read-elisp' does not support any of the following character read
+  ;; syntaxes:
+  ;;
+  ;; ?\N{NAME} (returns same as ?\N), ?\N{U+X} (returns same as ?\N), or \xXX
+  ;; (unsupported, signals error)
+  (define (sanitize-quasiquote exp)
+    (fold-right/elisp (lambda (t s)
+                        (match t
+                          ;; XXX: Prevent `(a b . ,c) -> `(a b #{,} c)
+                          ('#{,}# 'unquote)
+                          ('nil #f)
+                          ('t #t)
+                          (_ t)))
+                      (lambda (t s)
+                        (let ((s* (if (not t)
+                                      (list->dotted-list s)
+                                      s)))
+                          (match s*
+                            (`(#{`}# . ,rest)
+                             (cons* 'quasiquote rest))
+                            (`(#{,}# . ,rest)
+                             (cons* 'unquote rest))
+                            (`(#{,@}# . ,rest)
+                             (cons* 'unquote-splicing rest))
+                            (_ s*))))
+                      cons
+                      '()
+                      exp))
+
+  (sanitize-quasiquote (read-elisp port)))
+
+(define (read-elisp/sequence port)
+  "Read Elisp from PORT until the end-of-file is reached and return the list
+of expressions that were read."
+  (let loop ((lst '()))
+    (match (read-elisp* port)
+      ((? eof-object?)
+       (reverse! lst))
+      (`(%set-lexical-binding-mode ,(or #f #t))
+       (loop lst))
+      (exp
+       (loop (cons exp lst))))))
+
+(define (input->home-emacs-packages-configuration port)
+  "Return a `home-emacs-packages-configuration' record from Elisp read from
+PORT."
+  (schemified-elisp->home-emacs-packages-configuration
+   (read-elisp/sequence port)))
+
+(define (input->home-emacs-packages-configuration-sexp port)
+  "Return an s-expression defining a `home-emacs-packages-configuration'
+record from Elisp read from PORT."
+  (home-emacs-packages-configuration->code
+   (input->home-emacs-packages-configuration port)))
+
+;;; emacs.scm ends here
diff --git a/gnu/local.mk b/gnu/local.mk
index 997b7344ff..98bb48d25c 100644
--- a/gnu/local.mk
+++ b/gnu/local.mk
@@ -71,6 +71,7 @@
 # Copyright © 2024 Runciter <[email-masked]>
 # Copyright © 2024 Ashvith Shetty <[email-masked]>
 # Copyright © 2024 James Smith <[email-masked]>
+# Copyright © 2025 Kierin Bell <[email-masked]>
 #
 # This file is part of GNU Guix.
 #
@@ -106,6 +107,7 @@ GNU_SYSTEM_MODULES =				\
   %D%/home/services/desktop.scm			\
   %D%/home/services/dict.scm			\
   %D%/home/services/dotfiles.scm		\
+  %D%/home/services/emacs.scm			\
   %D%/home/services/symlink-manager.scm		\
   %D%/home/services/fontutils.scm		\
   %D%/home/services/gnupg.scm			\
diff --git a/guix/formatters/elisp.scm b/guix/formatters/elisp.scm
new file mode 100644
index 0000000000..032dd9e99e
--- /dev/null
+++ b/guix/formatters/elisp.scm
@@ -0,0 +1,918 @@
+;;; GNU Guix --- Functional package management for GNU
+;;; Copyright © 2021-2023 Ludovic Courtès <[email-masked]>
+;;; Copyright © 2023-2025 Kierin Bell <[email-masked]>
+;;;
+;;; This file is part of GNU Guix.
+;;;
+;;; GNU Guix is free software; you can redistribute it and/or modify it
+;;; under the terms of the GNU General Public License as published by
+;;; the Free Software Foundation; either version 3 of the License, or (at
+;;; your option) any later version.
+;;;
+;;; GNU Guix is distributed in the hope that it will be useful, but
+;;; WITHOUT ANY WARRANTY; without even the implied warranty of
+;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;;; GNU General Public License for more details.
+;;;
+;;; You should have received a copy of the GNU General Public License
+;;; along with GNU Guix.  If not, see <http://www.gnu.org/licenses/>.
+
+(define-module (guix formatters elisp)
+  #:use-module (ice-9 control)
+  #:use-module (ice-9 match)
+  #:use-module (ice-9 rdelim)
+  #:use-module (ice-9 vlist)
+  #:use-module (ice-9 format)
+  #:use-module (srfi srfi-1)
+  #:use-module (srfi srfi-26)
+  #:use-module (srfi srfi-34)
+  #:use-module (srfi srfi-35)
+  #:use-module (guix read-print)
+  #:use-module (guix i18n)
+  #:use-module ((guix diagnostics)
+                #:select (formatted-message
+                          &fix-hint &error-location
+                          location))
+  #:export (pretty-print-elisp
+            pretty-print-elisp/splice))
+
+;;; Commentary:
+;;;
+;;; Currently, this is adapted from the pretty printer in (guix read-print).
+;;; In the future, it would be better to implement this à la SRFI-159/166.
+;;;
+;;; Code:
+
+(define-syntax vhashq
+  (syntax-rules (quote)
+    ((_) vlist-null)
+    ((_ (key (quote (lst ...))) rest ...)
+     (vhash-consq key '(lst ...) (vhashq rest ...)))
+    ((_ (key value) rest ...)
+     (vhash-consq key '((() . value)) (vhashq rest ...)))))
+
+(define %elisp-special-forms
+  ;; Forms that should be indented specially in Elisp, adapted from the
+  ;; 'lisp-indent-function' property for each symbol by adding 1 to each
+  ;; integer value (and substituting 3 for `defun').  This is a non-exhaustive
+  ;; list, generated by mapping over the obarray of a minimal Emacs
+  ;; environment, and then removing symbols that are obsolete or unlikely to
+  ;; ever appear in an Emacs package or configuration file.
+  (vhashq
+   ('and-let* 2)
+   ('atomic-change-group 1)
+   ('autoload 3)
+   ('benchmark-progn 1)
+   ('benchmark-run 2)
+   ('benchmark-run-compiled 2)
+   ('byte-compile-maybe-guarded 2)
+   ('catch 2)
+   ('cl-block 2)
+   ('cl-callf 3)
+   ('cl-callf2 4)
+   ('cl-case 2)
+   ('cl-defgeneric 3)
+   ('cl-define-compiler-macro 3)
+   ('cl-defmacro 3)
+   ('cl-defmethod 4)
+   ('cl-defstruct 2)
+   ('cl-defsubst 3)
+   ('cl-deftype 3)
+   ('cl-defun 3)
+   ('cl-destructuring-bind 3)
+   ('cl-do 3)
+   ('cl-do* 3)
+   ('cl-do-all-symbols 2)
+   ('cl-do-symbols 2)
+   ('cl-dolist 2)
+   ('cl-dotimes 2)
+   ('cl-ecase 2)
+   ('cl-etypecase 2)
+   ('cl-eval-when 2)
+   ('cl-flet 2)
+   ('cl-flet* 2)
+   ('cl-generic-define-context-rewriter 4)
+   ('cl-generic-define-generalizer 2)
+   ('cl-iter-defun 3)
+   ('cl-labels 2)
+   ('cl-letf 2)
+   ('cl-letf* 2)
+   ('cl-macrolet 2)
+   ('cl-multiple-value-bind 3)
+   ('cl-multiple-value-setq 2)
+   ('cl-once-only 2)
+   ('cl-progv 3)
+   ('cl-return-from 2)
+   ('cl-symbol-macrolet 2)
+   ('cl-the 2)
+   ('cl-typecase 2)
+   ('cl-with-gensyms 2)
+   ('combine-after-change-calls 1)
+   ('combine-change-calls 3)
+   ('condition-case 3)
+   ('condition-case-unless-debug 3)
+   ('def-edebug-elem-spec 2)
+   ('def-edebug-spec 2)
+   ('defadvice 3)
+   ('defalias 3)
+   ('defclass 3)
+   ('defconst 3)
+   ('defcustom 3)
+   ('defface 3)
+   ('defgroup 3)
+   ('defimage 3)
+   ('define-abbrev 3)
+   ('define-abbrev-table 3)
+   ('define-advice 3)
+   ('define-alternatives 3)
+   ('define-auto-insert 3)
+   ('define-button-type 3)
+   ('define-category 3)
+   ('define-char-code-property 3)
+   ('define-derived-mode 3)
+   ('define-fringe-bitmap 3)
+   ('define-generic-mode 2)
+   ('define-globalized-minor-mode 3)
+   ('define-inline 3)
+   ('define-keymap 3)
+   ('define-mail-user-agent 3)
+   ('define-minor-mode 3)
+   ('define-multisession-variable 3)
+   ('define-obsolete-function-alias 3)
+   ('define-obsolete-variable-alias 3)
+   ('define-short-documentation-group 3)
+   ('define-skeleton 3)
+   ('define-widget 3)
+   ('define-widget-keywords 3)
+   ('defmacro 3)
+   ('defmath 3)
+   ('defsubst 3)
+   ('deftheme 2)
+   ('defun 3)
+   ('defvar 3)
+   ('defvar-keymap 2)
+   ('defvar-local 3)
+   ('defvaralias 3)
+   ('delay-mode-hooks 1)
+   ('dlet 2)
+   ('dolist 2)
+   ('dolist-with-progress-reporter 3)
+   ('dotimes 2)
+   ('dotimes-with-progress-reporter 3)
+   ('easy-menu-define 3)
+   ('easy-mmode-defmap 2)
+   ('easy-mmode-defsyntax 2)
+   ('ert-deftest 3)
+   ('eval-after-load 2)
+   ('eval-and-compile 1)
+   ('eval-when-compile 1)
+   ('gv-define-expander 2)
+   ('gv-define-setter 3)
+   ('gv-letplace 3)
+   ('if 2)                              ; Changed from 3
+   ('if-let 2)                          ; Changed from 3
+   ('if-let* 2)                         ; Changed from 3
+   ('ignore-error 2)
+   ('ignore-errors 1)
+   ('isearch-define-mode-toggle 4)
+   ('keymap-set-after 4)
+   ('lambda 2)                          ; Changed from 3
+   ('let 2)
+   ('let* 2)
+   ('let-alist 2)
+   ('let-when-compile 2)
+   ('letrec 2)
+   ('macroexp-let2 4)
+   ('macroexp-let2* 3)
+   ('minibuffer-with-setup-hook 2)
+   ('named-let 3)
+   ('oclosure-define 2)
+   ('oclosure-lambda 3)
+   ('pcase 2)
+   ('pcase-defmacro 3)
+   ('pcase-dolist 2)
+   ('pcase-exhaustive 2)
+   ('pcase-lambda 4)
+   ('pcase-let 2)
+   ('pcase-let* 2)
+   ('prog1 2)
+   ('prog2 3)
+   ('progn 1)
+   ('rx-define 3)
+   ('rx-let 2)
+   ('rx-let-eval 2)
+   ('save-current-buffer 1)
+   ('save-excursion 1)
+   ('save-mark-and-excursion 1)
+   ('save-match-data 1)
+   ('save-restriction 1)
+   ('save-selected-window 1)
+   ('save-window-excursion 1)
+   ('seq-doseq 2)
+   ('seq-let 3)
+   ('thread-first 1)
+   ('thread-last 1)
+   ('track-mouse 1)
+   ('unless 2)
+   ('unwind-protect 2)
+   ('use-package 2)                     ; Changed from 3
+   ('when 2)
+   ('when-let 2)
+   ('when-let* 2)
+   ('while 2)
+   ('while-let 2)
+   ('while-no-input 1)
+   ('with-auto-compression-mode 1)
+   ('with-buffer-unmodified-if-unchanged 1)
+   ('with-case-table 2)
+   ('with-category-table 2)
+   ('with-coding-priority 2)
+   ('with-current-buffer 2)
+   ('with-current-buffer-window 4)
+   ('with-decoded-time-value 2)
+   ('with-delayed-message 2)
+   ('with-demoted-errors 2)
+   ('with-displayed-buffer-window 4)
+   ('with-environment-variables 2)
+   ('with-eval-after-load 2)
+   ('with-existing-directory 1)
+   ('with-file-modes 2)
+   ('with-help-window 2)
+   ('with-local-quit 1)
+   ('with-locale-environment 2)
+   ('with-memoization 2)
+   ('with-minibuffer-completions-window 1)
+   ('with-minibuffer-selected-window 1)
+   ('with-mutex 2)
+   ('with-no-warnings 1)
+   ('with-output-to-string 1)
+   ('with-output-to-temp-buffer 2)
+   ('with-selected-frame 2)
+   ('with-selected-window 2)
+   ('with-silent-modifications 1)
+   ('with-slots 3)
+   ('with-suppressed-warnings 2)
+   ('with-syntax-table 2)
+   ('with-temp-buffer 1)
+   ('with-temp-buffer-window 4)
+   ('with-temp-file 2)
+   ('with-temp-message 2)
+   ('with-timeout 2)
+   ('with-undo-amalgamate 1)
+   ('with-window-non-dedicated 2)))
+
+(define (prefix? candidate lst)
+  "Return true if CANDIDATE is a prefix of LST."
+  (let loop ((candidate candidate)
+             (lst lst))
+    (match candidate
+      (() #t)
+      ((head1 . rest1)
+       (match lst
+         (() #f)
+         ((head2 . rest2)
+          (and (equal? head1 head2)
+               (loop rest1 rest2))))))))
+
+(define* (special-form-lead symbol context
+                            #:key
+                            (special-forms '()))
+  "If SYMBOL is an Elisp special form in the given CONTEXT, return its number
+of arguments; otherwise return #f.  CONTEXT is a stack of symbols lexically
+surrounding SYMBOL.  If SYMBOL is a key in the alist SPECIAL-FORMS, return the
+value of the first matching alist entry instead."
+  ;; A value N in SPECIAL-FORMS is equivalent to a value of N+1 in the
+  ;; '%elisp-special-forms' vhashe; this makes SPECIAL-FORMS similar to the
+  ;; 'lisp-indent-function' symbol property in Emacs and probably less
+  ;; confusing.
+  (or (assq-ref special-forms symbol)
+      (match (vhash-assq symbol %elisp-special-forms)
+        (#f #f)
+        ((_ . alist)
+         (any (match-lambda
+                ((prefix . level)
+                 (and (prefix? prefix context) (- level 1))))
+              alist)))))
+
+(define (escaped-string str)
+  "Return STR with backslashes and double quotes escaped.  Everything else, in
+particular newlines, is left as is."
+  (list->string
+   `(#\"
+     ,@(string-fold-right (lambda (chr lst)
+                            (match chr
+                              (#\" (cons* #\\ #\" lst))
+                              (#\\ (cons* #\\ #\\ lst))
+                              (_   (cons chr lst))))
+                          '()
+                          str)
+     #\")))
+
+(define %elisp-natural-whitespace-string-forms
+  ;; When a string has one of these forms as its parent, only double quotes
+  ;; and backslashes are escaped; newlines, tabs, etc. are left as-is.
+  '(defun defmacro))
+
+(define* (printed-string str context)
+  "Return the read syntax for STR depending on CONTEXT."
+  (define (preserve-newlines? str)
+    (and (> (string-length str) 40)
+         (string-index str #\newline)))
+
+  (match context
+    (()
+     (if (preserve-newlines? str)
+         (escaped-string str)
+         (object->string str)))
+    ((head . _)
+     (if (or (memq head %elisp-natural-whitespace-string-forms)
+             (preserve-newlines? str))
+         (escaped-string str)
+         (object->string str)))))
+
+(define (string-width str)
+  "Return the \"width\" of STR--i.e., the width of the longest line of STR."
+  (apply max (map string-length (string-split str #\newline))))
+
+(define %not-newline
+  (char-set-complement (char-set #\newline)))
+
+(define (print-multi-line-comment str indent port)
+  "Print to PORT STR as a multi-line comment, with INDENT spaces preceding
+each line except the first one (they're assumed to be already there)."
+  (let loop ((lst (string-tokenize str %not-newline)))
+    (match lst
+      (() #t)
+      ((last)
+       (display last port)
+       (newline port))
+      ((head tail ...)
+       (display head port)
+       (newline port)
+       (display (make-string indent #\space) port)
+       (loop tail)))))
+
+(define %elisp-special-symbol-chars
+  ;; Characters that need to be backslash-escaped within an Elisp symbol (see
+  ;; (elisp) Symbol Type).
+  (char-set-complement (char-set-union char-set:letter+digit
+                                       (char-set #\- #\+ #\= #\( #\/
+                                                 #\_ #\~ #\! #\@ #\$
+                                                 #\% #\^ #\& #\: #\<
+                                                 #\> #\{ #\} #\? #\*))))
+
+(define %elisp-confusable-number-symbols
+  ;; Symbols that must begin with a backslash in order to prevent them from
+  ;; being read as Elisp numbers.
+  (make-regexp (string-append
+                "(^[+-]?[0-9]+(\\.[0-9]*[eE]?(\\+NaN|\\+INF|[0-9]+)?)?$)"
+                "|(^[0-9]+[eE][0-9]+$)")))
+
+(define* (symbol->display-string symbol)
+  "Return the most appropriate representation of SYMBOL."
+  (let ((str (list->string
+              (string-fold-right
+               (lambda (chr lst)
+                 (if (char-set-contains? %elisp-special-symbol-chars chr)
+                     (cons* #\\ chr lst)
+                     (cons chr lst)))
+               '()
+               (symbol->string symbol)))))
+    (if (regexp-exec %elisp-confusable-number-symbols str)
+        (string-append "\\" str)
+        str)))
+
+(define %elisp-basic-chars
+  ;; Characters that can safely be specified using the Elisp character read
+  ;; syntax without backslash-escapes.
+  (char-set-union char-set:letter+digit
+                  (char-set #\~ #\! #\@ #\$ #\% #\^
+                            #\& #\* #\- #\_ #\= #\+
+                            #\{ #\} #\/ #\? #\< #\>)))
+
+(define %elisp-simple-escape-chars
+  ;; Whitespace, control, and other special characters that can be specified
+  ;; using the `?\X' Elisp read syntax, where X is a single character that has
+  ;; a special meaning.
+  (char-set #\alarm #\backspace #\tab #\newline #\vtab #\page #\return
+            #\esc #\space #\\ #\delete))
+
+(define (atom->elisp-string obj)
+  "Return a string representation of atom OBJ that is suitable for the Emacs
+Lisp reader.  Pairs and arrays should be serialized with 'pretty-print-elisp'
+instead."
+  (match obj
+    (#t
+     "t")
+    (() "()")
+    ((? nil?)
+     "nil")
+    ((? char?)
+     (cond
+      ((char-set-contains? %elisp-basic-chars obj)
+       (list->string (list #\? obj)))
+      ((char-set-contains? %elisp-simple-escape-chars obj)
+       (list->string (list #\? #\\ (case obj
+                                     ((#\alarm) #\a)
+                                     ((#\backspace) #\b)
+                                     ((#\tab) #\t)
+                                     ((#\newline) #\n)
+                                     ((#\vtab) #\v)
+                                     ((#\page) #\f)
+                                     ((#\return) #\r)
+                                     ((#\esc) #\e)
+                                     ((#\space) #\s)
+                                     ((#\\) #\\)
+                                     ((#\delete) #\d)))))
+      (else
+       (let ((num (char->integer obj)))
+         (if (<= num 65535)
+             (format #f "?\\u~4,'0x" num)
+             (format #f "?\\U~:@(~8,'0x~)" num))))))
+    ((? string?)
+     (printed-string obj '()))
+    ((? symbol?)
+     (symbol->display-string obj))
+    ((? keyword?)
+     (string-append ":" (symbol->display-string (keyword->symbol obj))))
+    ((? number? num)
+     (match num
+       ((? exact-integer?)
+        ;; E.g., 123
+        (object->string num))
+       ((? exact?)
+        ;; E.g., 1/2
+        (object->string (exact->inexact num)))
+       ((? rational?)
+        ;; E.g., 1.5
+        (object->string num))
+       ((? nan?)
+        ;; Not implemented by `read-elisp'.
+        "0.0e+NaN")
+       ((? inf?)
+        ;; Not implemented by `read-elisp'.
+        (if (negative? num)
+            "-1.0e+INF"
+            "1.0e+INF"))
+       (_
+        ;; Complex numbers
+        (raise
+         (formatted-message (G_ "cannot serialize complex number to Elisp: ~a")
+                            num)))))
+    (_
+     ;; Not an atom.
+     (raise
+      (formatted-message (G_ "Error serializing object to Elisp: ~a")
+                         obj)))))
+
+(define* (pretty-print-elisp port obj
+                             #:key
+                             (format-comment
+                              (lambda (comment indent) comment))
+                             (format-vertical-space identity)
+                             (indent 0)
+                             (max-width 78)
+                             (long-list 5)
+                             (special-forms '()))
+  "Pretty-print OBJ to PORT as Elisp, attempting to use at most MAX-WIDTH
+character columns and assuming the current column is INDENT.  Comments present
+in OBJ are included in the output.
+
+Lists longer than LONG-LIST are written as one element per line.  Comments are
+passed through FORMAT-COMMENT before being emitted; a useful value for
+FORMAT-COMMENT is 'canonicalize-comment'.  Vertical space is passed through
+FORMAT-VERTICAL-SPACE; a useful value of 'canonicalize-vertical-space'.
+
+To specify additional rules for special indentation, use SPECIAL-FORMS, an
+association list where each entry is a pair of the form (SYMBOL . INDENT).
+When SYMBOL occurs at the beginning of a list in OBJ, the first INDENT
+expressions after SYMBOL will be indented as arguments and the rest will be
+indented as body expressions.  Arguments that cannot be printed on the same
+line as SYMBOL will be indented 4 columns beyond the base indentation of the
+enclosing list, and body expressions will be indented 2 columns beyond the
+base indentation."
+  (define (read-syntax? obj)
+    (memq obj '(quote
+                quasiquote
+                unquote
+                unquote-splicing
+                ;; Elisp-only special forms:
+                function)))
+
+  (define (list-of-lists? head tail)
+    ;; Return true if HEAD and TAIL denote a list of lists--e.g., a list of
+    ;; 'let' bindings or an alist.
+    (match head
+      ((thing . _)
+       (and (not (read-syntax? thing))
+            (match tail
+              (((? pair?) . _)
+               #t)
+              (_ #f))))
+      (_ #f)))
+
+  (define list?*
+    (match-lambda
+      (((not (? read-syntax?)) . _)
+       #t)
+      (_ #f)))
+
+  (define (starts-with-line-comment? lst)
+    ;; Return true if LST starts with a line comment.
+    (match lst
+      ((x . _) (and (comment? x) (not (comment-margin? x))))
+      (_ #f)))
+
+  (define (array?* obj)
+    (and (array? obj)
+         (not (string? obj))))
+
+  (define (length* x)
+    ;; Return the length of list or dotted list X.
+    (let lp ((lst x)
+	     (len 0))
+      (match lst
+        (()
+         len)
+	((not (? pair?))
+	 (+ len 1))
+	((head . tail)
+	 (lp tail (+ len 1))))))
+
+  (define (dotted-list->list exp)
+    (let lp ((lst exp)
+	     (acc '()))
+      (match lst
+        (()
+         (reverse acc))
+	((not (? pair?))
+         (lp '() (cons lst acc)))
+	((head . tail)
+	 (lp tail (cons head acc))))))
+
+  (let loop ((indent indent)
+             (column indent)
+             (delimited? #t)
+             (context '())
+             (obj obj))
+    (define (print-sequence context indent column lst delimited?
+                            force-newline?)
+      (define dotted? (dotted-list? lst))
+      (define long?
+        ;; For lists that are function calls, omit heads from long list count,
+        ;; but include them for lists that aren't function calls.
+        (> (+ (length* lst)
+              (if (or dotted?
+                      (match context
+                        (((not (? symbol?)) . _)
+                         #t)
+                        ((_ 'quote . _)
+                         #t)
+                        (_ #f)))
+                  1 0))
+           long-list))
+
+      (let print ((lst lst)
+                  (first? #t)
+                  (delimited? delimited?)
+                  (column column)
+                  (unquote? #f)         ; End of list when, e.g., `(a b . ,c).
+                  (kw? #f))             ; Previous item was a keyword.
+        (cond
+         ((null? lst)
+          column)
+         ((blank? lst)
+          ;; Comments or whitespace cannot occur at the end of a dotted list.
+          column)
+         ((or unquote? (not (pair? lst)))
+          ;; End of improper list.
+          (let ((newline? (or long?
+                              (sequence-would-protrude?
+                               (+ column 2 (if unquote? 1 0))
+                               lst)
+                              (read-syntax-would-protrude?
+                               (+ column 2 (if unquote? 1 0))
+                               lst))))
+            (if newline?
+                (begin
+                  (newline port)
+                  (display (make-string indent #\space) port))
+                (display " " port))
+            (display ". " port)
+            (when unquote? (display "," port))
+            (let ((column (+ (if newline?
+                                 (+ indent 2)
+                                 (+ column 3))
+                             (if unquote? 1 0))))
+              (loop indent column #t context lst))))
+         (else
+          (match lst
+            (('unquote obj)
+             ;; A form like `(a b . ,OBJ) was expanded into (quasiquote (a b
+             ;; unquote OBJ)), which will still be properly expanded by
+             ;; `quasiquote' into (a b . OBJ).
+             (print obj #f #f column #t kw?))
+            ((item . tail)
+             (define kw-item?*
+               (cond
+                ((keyword? item) #t)
+                ((symbol? item) (string-prefix? ":" (symbol->string item)))
+                (else #f)))
+             (define newline?
+               ;; Insert a newline if ITEM is itself a list, or if TAIL is
+               ;; long, but only if ITEM is not the first item.  Also insert a
+               ;; newline before a keyword, and before a read syntax that
+               ;; would protrude.  We need to test before invocation of
+               ;; 'print-sequence' whether the first ITEM would protrude,
+               ;; since INDENT must then be less than usual.  We thread the
+               ;; results of that test to here with FORCE-NEWLINE?.
+               (or (and first? force-newline?)
+                   (and (or (list?* item)
+                            long?
+                            (read-syntax-would-protrude?
+                             (+ column 1) item)
+                            kw-item?*)
+                        (or dotted?     ; Newline after head of improper list.
+                            (not first?)
+                            (and first?
+                                 (match context
+                                   (((and (not (? symbol?))
+                                          (not (? keyword?))) . _)
+                                    ;; Allow newline before first item when
+                                    ;; head of list is not a symbol.
+                                    ;; E.g.:
+                                    ;; (use-package foo
+                                    ;;   :bind (("C-c f f" . foo) ;\n
+                                    ;;          :map foo-map
+                                    ;;          ("C-c f g" . foo-status)))
+                                    #t)
+                                   ((_ 'quote _ ...)
+                                    ;; E.g.:
+                                    ;; '(a ;\n
+                                    ;;   b)
+                                    #t)
+                                   (_ #f))))
+                        (not kw?)       ; Previous ITEM not a keyword.
+                        (not delimited?)
+                        (not (blank? item)))))
+
+             (when newline?
+               (newline port)
+               (display (make-string indent #\space) port))
+             (let ((column (if newline? indent column)))
+               (print tail
+                      #f
+                      (blank? item)
+                      (loop indent column
+                            (or newline? delimited?)
+                            context
+                            item)
+                      #f
+                      kw-item?*))))))))
+
+    (define (sequence-would-protrude? indent lst)
+      ;; Return true if elements of LST written at INDENT would protrude
+      ;; beyond MAX-WIDTH.  This is implemented as a cheap test with false
+      ;; negatives to avoid actually rendering all of LST.
+      (find (match-lambda
+              ((? string? str)
+               (>= (+ (string-width (printed-string str '()))
+                      2 indent)
+                   max-width))
+              ((? symbol? symbol)
+               (>= (+ (string-width (symbol->display-string symbol))
+                      indent)
+                   max-width))
+              ((? boolean?)
+               (>= (+ 2 indent) max-width))
+              (()
+               (>= (+ 2 indent) max-width))
+              (_                        ;don't know
+               #f))
+            (if (dotted-list? lst) (dotted-list->list lst) lst)))
+
+    (define (read-syntax-would-protrude? indent lst)
+      (match lst
+        ((or ((? read-syntax? syntax) exp)
+             (((? read-syntax? syntax) exp) . _))
+         (sequence-would-protrude? (+ indent (case syntax
+                                               ((quote) 1)
+                                               ((unquote) 1)
+                                               (else 2)))
+                                   exp))
+        (_ #f)))
+
+    (define (special-form-lead* head)
+      (special-form-lead head context
+                         #:special-forms special-forms))
+
+    (define (special-form? head)
+      (special-form-lead* head))
+
+    (match obj
+      ((? comment? comment)
+       (if (comment-margin? comment)
+           (begin
+             (display " " port)
+             (display (comment->string (format-comment comment indent))
+                      port))
+           (begin
+             ;; When already at the beginning of a line, for example because
+             ;; COMMENT follows a margin comment, no need to emit a newline.
+             (unless (= column indent)
+               (newline port)
+               (display (make-string indent #\space) port))
+             (print-multi-line-comment (comment->string
+                                        (format-comment comment indent))
+                                       indent port)))
+       (display (make-string indent #\space) port)
+       indent)
+      ((? vertical-space? space)
+       (unless delimited? (newline port))
+       (let loop ((i (vertical-space-height (format-vertical-space space))))
+         (unless (zero? i)
+           (newline port)
+           (loop (- i 1))))
+       (display (make-string indent #\space) port)
+       indent)
+      ((? page-break?)
+       (unless delimited? (newline port))
+       (display #\page port)
+       (newline port)
+       (display (make-string indent #\space) port)
+       indent)
+      (('quote lst)
+       (unless delimited? (display " " port))
+       (display "'" port)
+       (loop indent (+ column (if delimited? 1 2))
+             #t (cons 'quote context) lst))
+      (('quasiquote lst)
+       (unless delimited? (display " " port))
+       (display "`" port)
+       (loop indent (+ column (if delimited? 1 2)) #t context lst))
+      (('unquote lst)
+       (unless delimited? (display " " port))
+       (display "," port)
+       (loop indent (+ column (if delimited? 1 2)) #t context lst))
+      (('unquote-splicing lst)
+       (unless delimited? (display " " port))
+       (display ",@" port)
+       (loop indent (+ column (if delimited? 2 3)) #t context lst))
+      (('function lst)
+       (unless delimited? (display " " port))
+       (display "#'" port)
+       (loop indent (+ column (if delimited? 2 3)) #t context lst))
+      (((? special-form? head) arguments ...)
+       ;; Special-case 'let', 'lambda', 'modify-inputs', etc. so the second
+       ;; and following arguments are less indented.
+       (let* ((lead    (special-form-lead* head))
+              (context (cons head context))
+              (head    (symbol->display-string head))
+              (total   (length arguments))
+              (body    (drop arguments (min lead total))))
+         (unless delimited? (display " " port))
+         (display "(" port)
+         (display head port)
+         (unless (zero? lead)
+           (display " " port))
+
+         ;; Print the first LEAD arguments.
+         (let* ((indent (+ column 2 (if delimited? 0 1)))
+                (old-column column)
+                (column (+ column 1
+                                  (if (zero? lead) 0 1)
+                                  (if delimited? 0 1)
+                                  (string-length head)))
+                (initial-indent (+ old-column 4 (if delimited? 0 1))))
+           (define new-column
+             (let inner ((n lead)
+                         (arguments (take arguments (min lead total)))
+                         (column column)
+                         (newline? #f))
+               (if (zero? n)
+                   (if (null? body)     ; No newline when body is empty.
+                       column
+                       (begin
+                         (newline port)
+                         (display (make-string indent #\space) port)
+                         indent))
+                   (match arguments
+                     (() column)
+                     ((head . tail)
+                      (when newline?
+                        ;; Print a newline when previous argument was a list.
+                        (newline port)
+                        (display (make-string initial-indent #\space) port))
+                      (inner (- n 1) tail
+                             (loop initial-indent
+                                   (if newline? initial-indent column)
+                                   (or newline? (= n lead))
+                                   context
+                                   head)
+                             (list?* head)))))))
+
+           ;; Print the remaining arguments.
+           (let ((column (print-sequence
+                          context indent new-column
+                          body
+                          #t #f)))
+             (display ")" port)
+             (+ column 1)))))
+      ((? array?* obj)
+       ;; Vectors, arrays, bytevectors, bitvectors.
+       (let* ((lst (array->list obj))
+              (overflow? (>= column max-width))
+              (column (if overflow?
+                          (+ indent 1)
+                          (+ column (if delimited? 1 2)))))
+         (if overflow?
+             (begin
+               (newline port)
+               (display (make-string indent #\space) port))
+             (unless delimited? (display " " port)))
+         (display "[" port)
+         (let ((column (print-sequence context column column lst #t #f)))
+           (display "]" port)
+           (+ column 1))))
+      ((head . tail)
+       ;; Lists and improper lists.
+       (let* ((overflow? (>= column max-width))
+              (column (if overflow?
+                          (+ indent 1)
+                          (+ column (if delimited? 1 2))))
+              ;; Newline for 'let' bindings, alists, long lists of constants.
+              (newline? (or (and (not (null? tail))
+                                 (list-of-lists? head tail))
+                            (starts-with-line-comment? tail)))
+              (context (cons head context)))
+         (if overflow?
+             (begin
+               (newline port)
+               (display (make-string indent #\space) port))
+             (unless delimited? (display " " port)))
+         (display "(" port)
+
+         (let* ((new-column (loop column column #t context head))
+                (force-newline? (and (not newline?)
+                                     (or (read-syntax-would-protrude?
+                                          (+ new-column 1) tail)
+                                         (match tail
+                                           (((and lst
+                                                  ((not (? read-syntax?)) . _))
+                                             . _)
+                                            ;; Newline before initial list
+                                            ;; argument with long element(s).
+                                            (sequence-would-protrude?
+                                             (+ new-column 1) lst))
+                                           (_ #f)))))
+                (indent (if (or (>= new-column max-width)
+                                force-newline?
+                                newline?
+                                (not (symbol? head))
+                                (match context
+                                  ((_ 'quote _ ...)
+                                   #t)
+                                  (_ #f))
+                                (dotted-list? (cons head tail))
+                                (sequence-would-protrude?
+                                 (+ new-column 1) tail))
+                            column
+                            (+ new-column 1))))
+           (when newline?
+             ;; Insert a newline right after HEAD.
+             (newline port)
+             (display (make-string indent #\space) port))
+           (let ((column
+                  (print-sequence context indent
+                                  (if newline? indent new-column)
+                                  tail newline? force-newline?)))
+             (display ")" port)
+             (+ column 1)))))
+      (_
+       (let* ((str (cond ((string? obj)
+                          (printed-string obj context))
+                         ((symbol? obj) (symbol->display-string obj))
+                         (else (atom->elisp-string obj))))
+              (len (string-width str)))
+         (if (and (> (+ column 1 len) max-width)
+                  (not delimited?))
+             (begin
+               (newline port)
+               (display (make-string indent #\space) port)
+               (display str port)
+               (+ indent len))
+             (begin
+               (unless delimited? (display " " port))
+               (display str port)
+               (+ column (if delimited? 0 1) len))))))))
+
+(define* (pretty-print-elisp/splice port lst
+                                    #:rest rest)
+  "Write to PORT the expressions and blanks listed in LST."
+  (for-each (lambda (exp)
+              (apply pretty-print-elisp port exp rest)
+              (unless (blank? exp)
+                (newline port)))
+            lst))
+
+;;; elisp.scm ends here
diff --git a/guix/read-print.scm b/guix/read-print.scm
index e8caa2cfef..f5659716c2 100644
--- a/guix/read-print.scm
+++ b/guix/read-print.scm
@@ -374,6 +374,19 @@ (define %newline-forms
    ('set-xorg-configuration '())
    ('services '(home-environment))
    ('home-bash-configuration '(service))
+   ('home-emacs-packages-service-type '(service))
+   ('home-emacs-packages-configuration '(service))
+   ('packages '(home-emacs-packages-configuration))
+   ('list '(packages home-emacs-packages-configuration))
+   ('emacs-package '(list packages))
+   ('elisp* '())
+   ('extra-after-load '(emacs-package))
+   ('extra-init '(emacs-package))
+   ('keys-global '(emacs-package))
+   ('keys-local '(emacs-package))
+   ('list '(keys-local emacs-package))
+   ('emacs-keymap '(list keys-local emacs-package))
+   ('options '(emacs-package))
    ('introduction '(channel))))
 
 (define (prefix? candidate lst)
diff --git a/guix/scripts/home/import.scm b/guix/scripts/home/import.scm
index 15b4bc9798..3e2a238372 100644
--- a/guix/scripts/home/import.scm
+++ b/guix/scripts/home/import.scm
@@ -28,6 +28,7 @@ (define-module (guix scripts home import)
   #:autoload   (guix scripts package) (manifest-entry-version-prefix)
   #:use-module (guix read-print)
   #:use-module (gnu packages)
+  #:use-module (gnu home services emacs)
   #:use-module (ice-9 match)
   #:use-module (ice-9 rdelim)
   #:use-module (ice-9 regex)
@@ -110,10 +111,32 @@ (define (generate-bash-configuration+modules destination-directory)
       (guix gexp)
       (gnu home services shells))))
 
+(define (generate-emacs-packages-configuration+modules destination-directory)
+  "Return a list containing a 'home-emacs-packages-service-type' definition
+and the necessary modules for the definition."
+  (let ((init-file (any (lambda (path)
+                          (let ((path* (string-append destination-directory
+                                                      "/" path)))
+                            (and (file-exists? path*)
+                                 path*)))
+                        (list ".emacs.el"
+                              ".emacs"
+                              ".emacs.d/init.el"
+                              ".config/emacs/init.el"))))
+    `((service home-emacs-packages-service-type
+               ,(call-with-input-file init-file
+                  (lambda (port)
+                    (input->home-emacs-packages-configuration-sexp port))))
+      (gnu home services emacs))))
+
 (define %files+configurations-alist
   `((".bashrc" . ,generate-bash-configuration+modules)
     (".bash_profile" . ,generate-bash-configuration+modules)
-    (".bash_logout" . ,generate-bash-configuration+modules)))
+    (".bash_logout" . ,generate-bash-configuration+modules)
+    (".emacs.el" . ,generate-emacs-packages-configuration+modules)
+    (".emacs" . ,generate-emacs-packages-configuration+modules)
+    (".emacs.d/init.el" . ,generate-emacs-packages-configuration+modules)
+    (".config/emacs/init.el" . ,generate-emacs-packages-configuration+modules)))
 
 (define (configurations+modules configuration-directory)
   "Return a list of procedures which when called, generate code for a home
diff --git a/tests/formatters/elisp.scm b/tests/formatters/elisp.scm
new file mode 100644
index 0000000000..044de74f84
--- /dev/null
+++ b/tests/formatters/elisp.scm
@@ -0,0 +1,168 @@
+;;; GNU Guix --- Functional package management for GNU
+;;; Copyright © 2025 Kierin Bell <[email-masked]>
+;;;
+;;; This file is part of GNU Guix.
+;;;
+;;; GNU Guix is free software; you can redistribute it and/or modify it
+;;; under the terms of the GNU General Public License as published by
+;;; the Free Software Foundation; either version 3 of the License, or (at
+;;; your option) any later version.
+;;;
+;;; GNU Guix is distributed in the hope that it will be useful, but
+;;; WITHOUT ANY WARRANTY; without even the implied warranty of
+;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;;; GNU General Public License for more details.
+;;;
+;;; You should have received a copy of the GNU General Public License
+;;; along with GNU Guix.  If not, see <http://www.gnu.org/licenses/>.
+
+(define-module (tests-formatters-elisp)
+  #:use-module (srfi srfi-64)
+  #:use-module (guix formatters elisp)
+  #:use-module (guix read-print)
+  #:use-module (gnu home services emacs))
+
+(define (read-elisp* . x)
+  (apply (@@ (gnu home services emacs) read-elisp*) x))
+
+(define-syntax-rule (test-pretty-print exp str args ...)
+  "Test pretty printed Elisp string from EXP against STR."
+  (test-equal str
+    (call-with-output-string
+      (lambda (port)
+        (pretty-print-elisp port exp args ...)))))
+
+(define-syntax-rule (test-pretty-print-roundtrip str args ...)
+  "Test equality after a round-trip where STR is passed to a modified
+'read-elisp' and the resulting sexp is passed to 'pretty-print-elisp'."
+  (test-equal str
+    (call-with-output-string
+      (lambda (port)
+        (let ((exp (call-with-input-string str
+                     read-elisp*)))
+          (pretty-print-elisp port exp args ...))))))
+
+
+(test-begin "pretty-print-elisp")
+
+;; Reading Elisp characters as Scheme characters not supported.
+(test-pretty-print #\a "?a")
+(test-pretty-print #\alarm "?\\a")
+(test-pretty-print #\x2014 "?\\u2014")
+
+;; Reading comments (and margin comments) not supported.
+(test-pretty-print (elisp->sexp
+                    (elisp (progn 'foo
+                                  (unelisp-comment ";; Comment\n")
+                                  'bar)))
+                   "(progn\n  'foo\n  ;; Comment\n  'bar)")
+
+(test-pretty-print-roundtrip "224")
+(test-pretty-print-roundtrip "224.5")
+(test-pretty-print-roundtrip "-224.5")
+(test-pretty-print-roundtrip "\"string\"")
+(test-pretty-print-roundtrip "symbol")
+(test-pretty-print-roundtrip "'quoted-symbol")
+(test-pretty-print-roundtrip "symbol\\.with\\,escapes")
+(test-pretty-print-roundtrip "123non-confusable-symbol")
+(test-pretty-print-roundtrip "\\123e0")
+(test-pretty-print-roundtrip ":keyword*")
+
+(test-pretty-print-roundtrip "(a b c)")
+(test-pretty-print-roundtrip "(a . b)")
+(test-pretty-print-roundtrip "(a b . c)")
+(test-pretty-print-roundtrip "`(a b ,c)")
+(test-pretty-print-roundtrip "`(a b . ,c)")
+(test-pretty-print-roundtrip "(a b 'c)")
+
+(test-pretty-print-roundtrip "\
+(foo arg1
+     #'longer-than
+     arg3 arg4)"
+                         #:max-width 15)
+(test-pretty-print-roundtrip "\
+(foo
+ (list
+  longer)
+ b c)"
+                         #:max-width 10)
+(test-pretty-print-roundtrip "\
+(foo
+ #'longer-than
+ arg1 arg2)"
+                         #:max-width 10)
+(test-pretty-print-roundtrip "\
+(a
+ #'longer-than
+ b . c)"
+                         #:max-width 10)
+
+(test-pretty-print-roundtrip "\
+(defun foo (x y)
+  (let ((z (+ x y)))
+    (* z z)))")
+
+(test-pretty-print-roundtrip "[a b c]")
+(test-pretty-print-roundtrip "\
+[long-symbol
+ b c d]"
+                             #:max-width 10)
+(test-pretty-print-roundtrip "\
+[(long
+  list xx)
+ b c d]"
+                             #:max-width 10)
+
+(test-pretty-print-roundtrip "\
+(defun foo ()
+  (dlet ((x '((a b . \"c\"))))
+    x))")
+
+(test-pretty-print-roundtrip "\
+(defvar foo value)")
+
+(test-pretty-print-roundtrip "\
+(defvar foo #'foo-function
+  \"Foo function.\")")
+
+(test-pretty-print-roundtrip "\
+(if (fboundp 'foo-function)
+  (ding)
+  (autoload #'foo-function \"foo\"
+    \"Return foo.\"))")
+
+(test-pretty-print-roundtrip "\
+(long-variable-name-with-function-value
+ #'long-function-name-with-hash-read-syntax)"
+                             #:max-width 78)
+(test-pretty-print-roundtrip "\
+(a b
+   c
+   d
+   e
+   f
+   g)"
+                             #:long-list 5)
+(test-pretty-print-roundtrip "\
+[a
+ b
+ c
+ d
+ e
+ f]"
+                             #:long-list 5)
+(test-pretty-print-roundtrip "\
+(use-package foo
+  :bind ((\"C-c n\" . foo))
+  :custom (foo-bar 'bar)
+  (foo-baz my--baz)
+  :init (ding))"
+                             #:special-forms '((use-package . 1)))
+(test-pretty-print-roundtrip "\
+(with-current-buffer-window (setq buf
+                                  (get-buffer-create buf-name))
+    (cd-absolute directory)
+    (call-process-shell-command \"ls -l | sort -t _ -k 2\" nil t)
+  (dired-virtual directory))")
+
+(test-end "pretty-print-elisp")
diff --git a/tests/home/services/emacs.scm b/tests/home/services/emacs.scm
new file mode 100644
index 0000000000..159128236a
--- /dev/null
+++ b/tests/home/services/emacs.scm
@@ -0,0 +1,306 @@
+;;; GNU Guix --- Functional package management for GNU
+;;; Copyright © 2023 Kierin Bell <[email-masked]>
+;;;
+;;; This file is part of GNU Guix.
+;;;
+;;; GNU Guix is free software; you can redistribute it and/or modify it
+;;; under the terms of the GNU General Public License as published by
+;;; the Free Software Foundation; either version 3 of the License, or (at
+;;; your option) any later version.
+;;;
+;;; GNU Guix is distributed in the hope that it will be useful, but
+;;; WITHOUT ANY WARRANTY; without even the implied warranty of
+;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;;; GNU General Public License for more details.
+;;;
+;;; You should have received a copy of the GNU General Public License
+;;; along with GNU Guix.  If not, see <http://www.gnu.org/licenses/>.
+
+(define-module (tests home services emacs)
+  #:use-module (gnu home services emacs)
+  #:use-module (guix store)
+  #:use-module (guix monads)
+  #:use-module (guix gexp)
+  #:use-module (guix derivations)
+  #:use-module (guix packages)
+  #:use-module (guix tests)
+  #:use-module (guix read-print)
+  #:use-module (gnu packages guile)
+  #:use-module (ice-9 textual-ports)
+  #:use-module (srfi srfi-64))
+
+(test-begin "emacs-home-services")
+
+;;; Test `elisp' syntax
+
+(test-equal "test `elisp' syntax, symbol"
+  't
+  (elisp->sexp (elisp t)))
+
+(test-equal "test `elisp' syntax, basic list"
+  '(a b c)
+  (elisp->sexp (elisp (a b c))))
+
+(test-equal "test `elisp' syntax, substitute symbol"
+  'a
+  (let ((foo 'a))
+    (elisp->sexp (elisp (unelisp foo)))))
+
+(test-equal "test `elisp' syntax, substitute splicing"
+  '(a b c)
+  (let ((foo '(a b c)))
+    (elisp->sexp (elisp ((unelisp-splicing foo))))))
+
+(test-equal "test `elisp' syntax, comment"
+  (comment ";comment\n")
+  (elisp->sexp (elisp (unelisp-comment ";comment\n"))))
+
+(test-equal "test `elisp' syntax, nested comment"
+  `(a ,(comment ";comment\n") b)
+  (elisp->sexp (elisp (a (unelisp-comment ";comment\n") b))))
+
+(test-equal "test `elisp' syntax, newline"
+  (vertical-space 0)
+  (elisp->sexp (elisp (unelisp-newline))))
+
+(test-equal "test `elisp' syntax, page break"
+  (page-break)
+  (elisp->sexp (elisp (unelisp-page-break))))
+
+(test-equal "elisp->sexp, nested <elisp> objects"
+  '(a (b c))
+  (elisp->sexp (elisp (a (unelisp (elisp (b (unelisp (elisp c)))))))))
+
+;;; Test Home import utility functions
+
+(define (home-emacs-packages-configuration->code . x)
+  (apply (@@ (gnu home services emacs)
+             home-emacs-packages-configuration->code) x))
+
+(define-syntax-rule (test-import-emacs-configuration str config)
+  "Test equality of 'home-emacs-packages-configuration' generated from Elisp
+string STR with record CONFIG"
+  (test-equal "Test Emacs packages Home configuration import"
+    (home-emacs-packages-configuration->code
+     (call-with-input-string str
+       input->home-emacs-packages-configuration))
+    (home-emacs-packages-configuration->code config)))
+
+(test-import-emacs-configuration
+ "(setq my--foo 1)
+(setq my--bar 'symbol)
+(setq my--baz (list 'a ;comment
+		    'b 'c))
+(setq my--quux '(a b . c))
+(setq my--quuux #'my--fun)
+(setopt foo-var my--foo)
+
+(foo-mode -1)
+(bar-mode 1)
+(baz-mode)
+(quux-mode my--foo)
+
+
+(bind-key* \"M-<up>\" 'scroll-down-line)
+(bind-key* \"M-<down>\" 'scroll-up-line t)
+;;; Top-level comment
+(global-set-key (kbd \"C-c b\") 'bar)
+(global-set-key [remap bar] 'baz)
+(keymap-global-set \"C-c v\" 'quux)
+(bind-key \"C-c c\" 'quuux)
+(bind-key [t] #'quuuux 'foo-map t)
+
+(use-package foo
+  :demand t
+  ;; Inconvenient comment
+  :hook prog-mode
+  :custom
+  (foo-bar 'baz)
+  (foo-baz baz)
+  :init
+  ;; Ding
+  (ding)
+
+  (message \"Ding\"))
+
+(use-package bar
+  :if (eq system-type 'gnu/linx)
+  :after foo
+  :load-path \"~/src/bar\"
+  :autoload bar-internal
+  :commands bar-status bar
+  :bind* ((\"C-x n\" . bar-status))
+  :bind ((\"C-c n\" . bar)
+	 :map bar-mode
+	 (\"C-@\" . bar-bar)
+	 :map bar-status-mode
+	 (\"C-n\" . bar-next)
+	 (\"C-c C-c\" . bar-do)
+	 :repeat-map bar-repeat-map
+	 (\"n\" . bar-next)
+	 (\"c\" . bar-do))
+  :bind-keymap (\"C-c b\" . bar-mode-map)
+  :custom
+  (bar-bool t)
+  (bar-string \"bar\")
+  (bar-list '(bar-1 bar-2 bar-3))
+  (bar-list-2 `(,@bar-list bar-4))
+  (bar-var my--foo)
+  :custom-face
+  (bar-face ((t (:slant italic))))
+  (bar-highlight-face ((((class color) (background light))
+			:background \"goldenrod1\")
+		       (((class color) (background dark))
+			:background \"DarkGoldenrod4\")
+		       (t :inverse-video t)))
+  :hook ((prog-mode foo-mode) . bar-mode)
+  :mode \"\\\\.bar\\\\'\"
+  :magic \">>BAR<<\"
+  :magic-fallback \"<<BAR>>\"
+  :config
+  ;; Extra configuration
+  (add-to-list 'bar-extensions 'foo-bar)
+  :catch (lambda (_ _)
+	   (message \"Error package initialization\")))
+
+(use-package baz
+  :unless (eq system-name \"bar\")
+  :after (foo bar)
+  :load-path (\"~/src/my/baz\" \"~/src/baz\")
+  :autoload (baz-1 baz-2)
+  :commands (baz)
+  :custom
+  ((baz-option t)
+   (bar-list '((baz-1 . baz-2)))
+   (baz-var my--foo))
+  :hook ((prog-mode . baz-mode)
+	 (bar-mode . baz-mode))
+  :mode (\"\\\\.baz\\\\'\" . baz-mode)
+  :magic (\">>BAZ<<\" \"!XXBAZXX\"))
+
+(defun my--fun-1 (arg)
+  arg)
+
+(defun my--fun ()
+  (prog1 (my--fun-1 'foo)
+    (ding)))
+"
+ (home-emacs-packages-configuration
+  (packages
+   (list
+    (emacs-package
+      (name 'emacs)
+      (keys-global
+       '(("C-c b" . bar)
+         (#(remap bar) . baz)
+         ("C-c v" . quux)
+         ("C-c c" . quuux)))
+      (keys-override
+       '(("M-<up>" . scroll-down-line)
+         ("M-<down>" . scroll-up-line)))
+      (keys-local
+       (list
+        (emacs-keymap
+         (name 'foo-map)
+         (keys '((#(t) . quuuux))))))
+      (options
+       `((my--foo . 1)
+         (my--bar . symbol)
+         (my--baz
+          . ,(elisp
+              (list 'a 'b 'c)))
+         (my--quux . (a b . c))
+         (my--quuux . ,(elisp (function my--fun)))
+         (foo-var . ,(elisp my--foo))))
+      (extra-init
+       (elisp*
+        (foo-mode -1)
+        (bar-mode 1)
+        (baz-mode)
+        (quux-mode my--foo)
+        (defun my--fun-1 (arg) arg)
+        (defun my--fun ()
+          (prog1 (my--fun-1 (quote foo)) (ding))))))
+    (emacs-package
+      (name 'foo)
+      (load-force? #t)
+      (options
+       `((foo-bar . baz)
+         (foo-baz . ,(elisp baz))))
+      (hooks '((prog-mode . foo-mode)))
+      (extra-init
+       (elisp*
+        (ding)
+        (message "Ding"))))
+    (emacs-package
+      (name 'bar)
+      (load-predicates
+       (list (elisp (eq system-type 'gnu/linx))))
+      (load-after-packages '(foo))
+      (load-paths '("~/src/bar"))
+      (autoloads '(bar-internal))
+      (autoloads-interactive '(bar-status bar))
+      (keys-global '(("C-c n" . bar)))
+      (keys-global-keymaps '(("C-c b" . bar-mode-map)))
+      (keys-override '(("C-x n" . bar-status)))
+      (keys-local
+       (list
+        (emacs-keymap
+         (name 'bar-mode)
+         (keys '(("C-@" . bar-bar))))
+        (emacs-keymap
+         (name 'bar-status-mode)
+         (keys '(("C-n" . bar-next)
+                 ("C-c C-c" . bar-do))))
+        (emacs-keymap
+         (name 'bar-repeat-map)
+         (repeat? #t)
+         (keys '(("n" . bar-next)
+                 ("c" . bar-do))))))
+      (options
+       `((bar-bool . #t)
+         (bar-string . "bar")
+         (bar-list . (bar-1 bar-2 bar-3))
+         (bar-list-2 . ,(elisp
+                         `(,@bar-list bar-4)))
+         (bar-var . ,(elisp my--foo))))
+      (faces
+       '((bar-face (t (:slant italic)))
+         (bar-highlight-face
+          . ((((class color) (background light))
+              :background "goldenrod1")
+             (((class color) (background dark))
+              :background "DarkGoldenrod4")
+             (t :inverse-video t)))))
+      (hooks '((prog-mode . bar-mode)
+               (foo-mode . bar-mode)))
+      (auto-modes '(("\\.bar\\'" . bar)))
+      (magic-modes '((">>BAR<<" . bar)))
+      (extra-after-load
+       (elisp*
+        (add-to-list 'bar-extensions 'foo-bar)))
+      (extra-keywords
+       `((:magic-fallback . (,(elisp "<<BAR>>")))
+         (:catch . (,(elisp
+                      (lambda (_ _)
+                        (message
+                         "Error package initialization"))))))))
+    (emacs-package
+      (name 'baz)
+      (load-predicates
+       (list (elisp (not (eq system-name "bar")))))
+      (load-after-packages '(foo bar))
+      (load-paths '("~/src/my/baz" "~/src/baz"))
+      (autoloads '(baz-1 baz-2))
+      (autoloads-interactive '(baz))
+      (options
+       `((baz-option . #t)
+         (bar-list . ((baz-1 . baz-2)))
+         (baz-var . ,(elisp my--foo))))
+      (hooks '((prog-mode . baz-mode)
+               (bar-mode . baz-mode)))
+      (auto-modes '(("\\.baz\\'" . baz-mode)))
+      (magic-modes '((">>BAZ<<" . baz)
+                     ("!XXBAZXX" . baz))))))))
+
+(test-end "emacs-home-services")

base-commit: 310adf4ce70cbb864859274fcc7842bd519bbddc
-- 
2.48.1