Welcome to the second part of the Guix deep dive! Today we will be looking a bit deeper into the operating-system record, mostly into system services, how they are implemented and how we can configure them.

Before you read further I strongly recommend reading the first part as it provides some context to what we will be doing now. I'll also be referring to the system configuration from that other post.

This time we'll look at the surface level of how Guix handles services and their configuration.

The default services

Yet again we can find the default service list by peeking into the operating-system record.

(services operating-system-user-services        ; list of services
          (thunked)                     ;allow for system-dependent services
          (default %base-services))

This time, let's actually find the service record first. Finding the scheme definition for services was really straightforward - it's at gnu/services.scm. Once inside the file we can look for the service record with a simple search and immediately the following definition is found.

;; Services of a given type.
(define-record-type <service>
  (make-service type value)
  service?
  (type       service-kind)
  (value      service-value))

As you can see services are actually only defined as 2 variables - the service type and value. Take note of the service-kind accessor, we're going to use it later.

Service type

Let's first take a look at the service type. Yet again we can look at the record definition.

(define-record-type* <service-type> service-type make-service-type
  service-type?
  (name       service-type-name)                  ;symbol (for debugging)

  ;; Things extended by services of this type.
  (extensions service-type-extensions)            ;list of <service-extensions>

  ;; Given a list of extensions, "compose" them.
  (compose    service-type-compose                ;list of Any -> Any
              (default #f))

  ;; Extend the services' own parameters with the extension composition.
  (extend     service-type-extend                 ;list of Any -> parameters
              (default #f))

  ;; Optional default value for instances of this type.
  (default-value service-type-default-value       ;Any
    (default &no-default-value))

  ;; Meta-data.
  (description  service-type-description)         ;string
  (location     service-type-location             ;<location>
                (default (and=> (current-source-location)
                                source-properties->location))
                (innate)))

Most slots here are pretty self-explanatory, aside from compose, extend and extensions. I'm going to refer to the Guix documentation as it provides a short and clear explanation of what these do - my explanation effort here would be redundant. Here's the link.

With this we can go ahead and dissect the %desktop-services which I use in my config. Let's start with printing their names, left in for debugging purposes

scheme@(guile-user)> (map (lambda (service) (service-type-name (service-kind service))) %desktop-services)
$9 = (gdm screen-locker screen-locker mtp sane polkit-wheel mount-setuid-helpers gdm-file-system fontconfig-file-system network-manager wpa-supplicant network-manager-applet modem-manager usb-modeswitch avahi udisks upower accountsservice cups-pk-helper colord geoclue polkit elogind dbus ntp x11-socket-directory-service pulseaudio alsa login virtual-terminal console-fonts syslog agetty mingetty mingetty mingetty mingetty mingetty mingetty static-networking urandom-seed guix nscd rottlog log-cleanup udev sysctl special-files)

We first extract the service type with service-kind, followed by extracting that types name with service-type-name.

This can already give us some ideas for how service lists can be modified, let's remove the gdm service and then print the service list.

scheme@(guile-user)>
  (map (lambda (service) (service-type-name (service-kind service))) 
       (remove (lambda (service) (equal? (service-type-name (service-kind service)) 'gdm)) %desktop-services))

$14 = (screen-locker screen-locker mtp sane polkit-wheel mount-setuid-helpers gdm-file-system fontconfig-file-system network-manager wpa-supplicant network-manager-applet modem-manager usb-modeswitch avahi udisks upower accountsservice cups-pk-helper colord geoclue polkit elogind dbus ntp x11-socket-directory-service pulseaudio alsa login virtual-terminal console-fonts syslog agetty mingetty mingetty mingetty mingetty mingetty mingetty static-networking urandom-seed guix nscd rottlog log-cleanup udev sysctl special-files)

Again we simply extract the service type name, then filter it by comparing it against the quoted gdm symbol. The map here is the same as above, used to print the list in a readable format. We can also remove services by comparing service types instead.

If we take a quick peek at the config we can see something like this:

(define %my-services
  (modify-services %desktop-services
    (delete gdm-service-type)
    (guix-service-type config => (guix-configuration
                                  (inherit config)
                                  (substitute-urls
                                   (append (list "https://substitutes.nonguix.org")
                                           %default-substitute-urls))
                                  (authorized-keys
                                   (append (list (local-file "./signing-key.pub"))
                                           %default-authorized-guix-keys))))
    (dbus-root-service-type config =>
                            (dbus-configuration (inherit config)
                                                (services (list libratbag blueman))))))

As you can see Guix provides a modify-services macro, which abstracts service deletion away, making our effort kind of unnecessary… but at least we now understand how that part of the macro works under the hood and we learned a little about service types along the way.

Looking into actual services

I think now is a good time to look at how an actual service is implemented. I'll use the gdm service again.

First of all let's find the definition of gdm-service-type. I'll use M-. to get there right away.

(define gdm-service-type
  (handle-xorg-configuration gdm-configuration
    (service-type (name 'gdm)
                  (extensions
                   (list (service-extension shepherd-root-service-type
                                            gdm-shepherd-service)
                         (service-extension account-service-type
                                            (const %gdm-accounts))
                         (service-extension dconf-service-type
                                            gdm-dconf-profiles)
                         (service-extension pam-root-service-type
                                            gdm-pam-service)
                         (service-extension polkit-service-type
                                            gdm-polkit-rules)
                         (service-extension profile-service-type
                                            gdm-configuration-gnome-shell-assets)
                         (service-extension dbus-root-service-type
                                            (compose list
                                                     gdm-configuration-gdm))
                         (service-extension localed-service-type
                                            (compose
                                             xorg-configuration-keyboard-layout
                                             gdm-configuration-xorg))))
                  (default-value (gdm-configuration))
                  (description
                   "Run the GNOME Desktop Manager (GDM), a program that allows
you to log in in a graphical session, whether or not you use GNOME."))))

Here we can see which services are extended by GDM.

The logical next step is the gdm-configuration which is another record that looks as follows:

(define-record-type* <gdm-configuration>
  gdm-configuration make-gdm-configuration
  gdm-configuration?
  (gdm gdm-configuration-gdm (default gdm))
  (allow-empty-passwords? gdm-configuration-allow-empty-passwords? (default #t))
  (auto-login? gdm-configuration-auto-login? (default #f))
  (auto-suspend? gdm-configuration-auto-suspend? (default #t))
  (dbus-daemon gdm-configuration-dbus-daemon (default dbus-daemon-wrapper))
  (debug? gdm-configuration-debug? (default #f))
  (default-user gdm-configuration-default-user (default #f))
  (gnome-shell-assets gdm-configuration-gnome-shell-assets
                      (default (list adwaita-icon-theme font-abattis-cantarell)))
  (xorg-configuration gdm-configuration-xorg
                      (default (xorg-configuration)))
  (x-session gdm-configuration-x-session
             (default (xinitrc)))
  (xdmcp? gdm-configuration-xdmcp?
          (default #f))
  (wayland? gdm-configuration-wayland? (default #f))
  (wayland-session gdm-configuration-wayland-session
                   (default gdm-wayland-session-wrapper)))

This gives us more insight into how the service can be configured.

The gdm slot is rather interesting here as it defines which package the service is going to use. We're going to apply this knowledge practically later.

If we want to look into how the service configuration is defined then gdm-shepherd-service is what we're looking for.

(define (gdm-shepherd-service config)
  (define config-file
    (gdm-configuration-file config))

  (list (shepherd-service
         (documentation "Xorg display server (GDM)")
         (provision '(xorg-server))
         (requirement '(dbus-system pam user-processes host-name udev elogind))
         (start #~(lambda ()
                    (fork+exec-command
                     (list #$(file-append (gdm-configuration-gdm config)
                                          "/bin/gdm"))
                     #:environment-variables
                     (list #$@(if (gdm-configuration-auto-suspend? config)
                                  #~()
                                  #~("DCONF_PROFILE=/etc/dconf/profile/gdm"))
                           (string-append "GDM_CUSTOM_CONF=" #$config-file)
                           (string-append
                            "GDM_DBUS_DAEMON="
                            #$(gdm-configuration-dbus-daemon config))
                           (string-append
                            "GDM_X_SERVER="
                            #$(xorg-wrapper
                               (gdm-configuration-xorg config)))
                           (string-append
                            "GDM_X_SESSION="
                            #$(gdm-configuration-x-session config))
                           (string-append
                            "XDG_DATA_DIRS="
                            ((lambda (ls) (string-join ls ":"))
                             (map (lambda (path)
                                    (string-append path "/share"))
                                  ;; XXX: Remove gnome-shell below when GDM
                                  ;; can depend on GNOME Shell directly.
                                  (cons #$gnome-shell
                                        '#$(gdm-configuration-gnome-shell-assets
                                            config)))))
                           ;; Add XCURSOR_PATH so that mutter can find its
                           ;; cursors.  gdm doesn't login so doesn't source
                           ;; the corresponding line in /etc/profile.
                           "XCURSOR_PATH=/run/current-system/profile/share/icons"
                           (string-append
                            "GDK_PIXBUF_MODULE_FILE="
                            #$gnome-shell "/" #$%gdk-pixbuf-loaders-cache-file)
                           (string-append
                            "GDM_WAYLAND_SESSION="
                            #$(gdm-configuration-wayland-session config))))))
         (stop #~(make-kill-destructor))
         (actions (list (shepherd-configuration-action config-file)))
         (respawn? #t))))

We can see how Guix composes the GDM configuration file and writes the configuration, as well as the start process for the service - in this case a simple invocation of the gdm command. It's also seen here that the GDM service provides the xorg-server service. This means if we added another instance of that to our config we would actually get a duplicate which is no good.

shepherd-service is a whole another can of worms - we'll learn about that some other time.

A practical usage example

I think we have already learned enough for now. We can use the newfound knowledge in all sorts of practical and interesting ways.

One of the common questions I've seen asked by Guix users is how one would go about stripping their desktop environment of unnecessary packages - so let's try that!

I'll be modifying Gnome as it ships with a lot of unnecessary stuff by default. Let's start with the gnome-desktop-service-type.

(define gnome-desktop-service-type
  (service-type
   (name 'gnome-desktop)
   (extensions
    (list (service-extension udev-service-type
                             gnome-udev-rules)
          (service-extension polkit-service-type
                             gnome-polkit-settings)
          (service-extension setuid-program-service-type
                             gnome-setuid-programs)
          (service-extension profile-service-type
                             (compose list gnome-desktop-configuration-gnome))))
   (default-value (gnome-desktop-configuration))
   (description "Run the GNOME desktop environment.")))

The gnome-desktop-configuration is what we want here, let's pull up its definition.

(define-record-type* <gnome-desktop-configuration> gnome-desktop-configuration
  make-gnome-desktop-configuration
  gnome-desktop-configuration?
  (gnome gnome-desktop-configuration-gnome
         (default gnome)))

And here we can see what we're looking for - the gnome slot. This slot defines which Gnome package is used for the service. If we replace this package with a stripped-down version, we can remove all the bloat we don't want.

So let's make that stripped-down version of Gnome and remove all the stuff we don't want. We haven't been through defining packages yet, so this here is a bit of magic we will investigate some other time.

(define my-gnome
  (package
   (inherit gnome)
   (name "my-gnome")
   (propagated-inputs
    (modify-inputs (package-propagated-inputs gnome)
                   (delete "epiphany")
                   (delete "gnome-weather")
                   (delete "simple-scan")
                   (delete "gnome-maps")
                   (delete "cheese")
                   (delete "gnome-boxes")
                   (delete "gnome-console")
                   (delete "gnome-contacts")
                   (append gnome-terminal)))))

We add this package definition to our system config, then move on to defining the service.

Now we construct service type such that is uses a custom gnome-configuration - which we tell to use our custom my-gnome package. This is practically all just playing around with the records we looked at above.

(operating-system
  ...
  (services
   (cons* 
    (service gnome-desktop-service-type
             (gnome-configuration
              (gnome my-gnome)))
    %desktop-services))
  ...
)

End of part 2

By now we can find almost every detail about a service without ever opening its documentation - a good example of how Guix is self-documenting. With this knowledge we have great power in customizing the system services but we unfortunately can't write our own yet.

Next time I'd like to look a little deeper into service definition and even define some basic services. After that we can move onto one of the most exciting parts - defining and building our own packages!