WIP: bees service
[jackhill/guix/guix.git] / gnu / services.scm
index 016ff08..e7da0a0 100644 (file)
@@ -1,6 +1,8 @@
 ;;; GNU Guix --- Functional package management for GNU
-;;; Copyright © 2015, 2016, 2017 Ludovic Courtès <ludo@gnu.org>
+;;; Copyright © 2015, 2016, 2017, 2018, 2019, 2020 Ludovic Courtès <ludo@gnu.org>
 ;;; Copyright © 2016 Chris Marusich <cmmarusich@gmail.com>
+;;; Copyright © 2020 Jan (janneke) Nieuwenhuizen <janneke@gnu.org>
+;;; Copyright © 2020, 2021 Ricardo Wurmus <rekado@elephly.net>
 ;;;
 ;;; This file is part of GNU Guix.
 ;;;
   #:use-module (guix records)
   #:use-module (guix profiles)
   #:use-module (guix discovery)
+  #:use-module (guix combinators)
+  #:use-module (guix channels)
+  #:use-module (guix describe)
   #:use-module (guix sets)
   #:use-module (guix ui)
-  #:use-module ((guix utils) #:select (source-properties->location))
+  #:use-module (guix diagnostics)
+  #:autoload   (guix openpgp) (openpgp-format-fingerprint)
   #:use-module (guix modules)
   #:use-module (gnu packages base)
   #:use-module (gnu packages bash)
+  #:use-module (gnu packages hurd)
   #:use-module (srfi srfi-1)
   #:use-module (srfi srfi-9)
   #:use-module (srfi srfi-9 gnu)
@@ -38,6 +45,7 @@
   #:use-module (srfi srfi-35)
   #:use-module (ice-9 vlist)
   #:use-module (ice-9 match)
+  #:autoload   (ice-9 pretty-print) (pretty-print)
   #:export (service-extension
             service-extension?
             service-extension-target
@@ -66,6 +74,7 @@
             simple-service
             modify-services
             service-back-edges
+            instantiate-missing-services
             fold-services
 
             service-error?
             ambiguous-target-service-error-target-type
 
             system-service-type
+            provenance-service-type
+            sexp->system-provenance
+            system-provenance
             boot-service-type
             cleanup-service-type
             activation-service-type
             activation-service->script
             %linux-bare-metal-service
+            %hurd-rc-script
+            %hurd-startup-service
             special-files-service-type
             extra-special-file
             etc-service-type
 
             %boot-service
             %activation-service
-            etc-service))
+            etc-service)
+  #:re-export (;; Note: Re-export 'delete' to allow for proper syntax matching
+               ;; in 'modify-services' forms.  See
+               ;; <https://debbugs.gnu.org/cgi/bugreport.cgi?bug=26805#16>.
+               delete))
 
 ;;; Comment:
 ;;;
 (define (all-service-modules)
   "Return the default set of service modules."
   (cons (resolve-interface '(gnu services))
-        (all-modules (%service-type-path))))
+        (all-modules (%service-type-path)
+                     #:warn warn-about-load-error)))
 
 (define* (fold-service-types proc seed
                              #:optional
@@ -229,13 +248,13 @@ TYPE does not have a default value, an error is raised."
     (if (eq? default &no-default-value)
         (let ((location (source-properties->location location)))
           (raise
-           (condition
-            (&missing-value-service-error (type type) (location location))
-            (&message
-             (message (format #f (G_ "~a: no value specified \
+           (make-compound-condition
+            (condition
+             (&missing-value-service-error (type type) (location location)))
+            (formatted-message (G_ "~a: no value specified \
 for service of type '~a'")
-                              (location->string location)
-                              (service-type-name type)))))))
+                               (location->string location)
+                               (service-type-name type)))))
         (service type default))))
 
 (define-condition-type &service-error &error
@@ -265,7 +284,11 @@ singleton service type NAME, of which the returned service is an instance."
     (service type value)))
 
 (define-syntax %modify-service
-  (syntax-rules (=>)
+  (syntax-rules (=> delete)
+    ((_ svc (delete kind) clauses ...)
+     (if (eq? (service-kind svc) kind)
+         #f
+         (%modify-service svc clauses ...)))
     ((_ service)
      service)
     ((_ svc (kind param => exp ...) clauses ...)
@@ -295,27 +318,29 @@ TYPE.  Consider this example:
     (mingetty-service-type config =>
                            (mingetty-configuration
                             (inherit config)
-                            (motd (plain-file \"motd\" \"Hi there!\")))))
+                            (motd (plain-file \"motd\" \"Hi there!\"))))
+    (delete udev-service-type))
 
 It changes the configuration of the GUIX-SERVICE-TYPE instance, and that of
-all the MINGETTY-SERVICE-TYPE instances.
+all the MINGETTY-SERVICE-TYPE instances, and it deletes instances of the
+UDEV-SERVICE-TYPE.
 
-This is a shorthand for (map (lambda (svc) ...) %base-services)."
+This is a shorthand for (filter-map (lambda (svc) ...) %base-services)."
     ((_ services clauses ...)
-     (map (lambda (service)
-            (%modify-service service clauses ...))
-          services))))
+     (filter-map (lambda (service)
+                   (%modify-service service clauses ...))
+                 services))))
 
 \f
 ;;;
 ;;; Core services.
 ;;;
 
-(define (system-derivation mentries mextensions)
+(define (system-derivation entries mextensions)
   "Return as a monadic value the derivation of the 'system' directory
 containing the given entries."
-  (mlet %store-monad ((entries    mentries)
-                      (extensions (sequence %store-monad mextensions)))
+  (mlet %store-monad ((extensions (mapm/accumulate-builds identity
+                                                          mextensions)))
     (lower-object
      (file-union "system"
                  (append entries (concatenate extensions))))))
@@ -328,13 +353,20 @@ containing the given entries."
   (service-type (name 'system)
                 (extensions '())
                 (compose identity)
-                (extend system-derivation)))
-
-(define (compute-boot-script _ mexps)
-  (mlet %store-monad ((gexps (sequence %store-monad mexps)))
-    (gexp->file "boot"
-                ;; Clean up and activate the system, then spawn shepherd.
-                #~(begin #$@gexps))))
+                (extend system-derivation)
+                (description
+                 "Build the operating system top-level directory, which in
+turn refers to everything the operating system needs: its kernel, initrd,
+system profile, boot script, and so on.")))
+
+(define (compute-boot-script _ gexps)
+  ;; Reverse GEXPS so that extensions appear in the boot script in the right
+  ;; order.  That is, user extensions would come first, and extensions added
+  ;; by 'essential-services' (e.g., running shepherd) are guaranteed to come
+  ;; last.
+  (gexp->file "boot"
+              ;; Clean up and activate the system, then spawn shepherd.
+              #~(begin #$@(reverse gexps))))
 
 (define (boot-script-entry mboot)
   "Return, as a monadic value, an entry for the boot script in the system
@@ -343,63 +375,217 @@ directory."
     (return `(("boot" ,boot)))))
 
 (define boot-service-type
-  ;; The service of this type is extended by being passed gexps as monadic
-  ;; values.  It aggregates them in a single script, as a monadic value, which
-  ;; becomes its 'parameters'.  It is the only service that extends nothing.
+  ;; The service of this type is extended by being passed gexps.  It
+  ;; aggregates them in a single script, as a monadic value, which becomes its
+  ;; value.
   (service-type (name 'boot)
                 (extensions
                  (list (service-extension system-service-type
                                           boot-script-entry)))
-                (compose append)
-                (extend compute-boot-script)))
+                (compose identity)
+                (extend compute-boot-script)
+                (description
+                 "Produce the operating system's boot script, which is spawned
+by the initrd once the root file system is mounted.")))
 
 (define %boot-service
   ;; The service that produces the boot script.
   (service boot-service-type #t))
 
+\f
+;;;
+;;; Provenance tracking.
+;;;
+
+(define (object->pretty-string obj)
+  "Like 'object->string', but using 'pretty-print'."
+  (call-with-output-string
+    (lambda (port)
+      (pretty-print obj port))))
+
+(define (channel->code channel)
+  "Return code to build CHANNEL, ready to be dropped in a 'channels.scm'
+file."
+  ;; Since the 'introduction' field is backward-incompatible, and since it's
+  ;; optional when using the "official" 'guix channel, include it if and only
+  ;; if we're referring to a different channel.
+  (let ((intro (and (not (equal? (list channel) %default-channels))
+                    (channel-introduction channel))))
+    `(channel (name ',(channel-name channel))
+              (url ,(channel-url channel))
+              (branch ,(channel-branch channel))
+              (commit ,(channel-commit channel))
+              ,@(if intro
+                    `((introduction
+                       (make-channel-introduction
+                        ,(channel-introduction-first-signed-commit intro)
+                        (openpgp-fingerprint
+                         ,(openpgp-format-fingerprint
+                           (channel-introduction-first-commit-signer
+                            intro))))))
+                    '()))))
+
+(define (channel->sexp channel)
+  "Return an sexp describing CHANNEL.  The sexp is _not_ code and is meant to
+be parsed by tools; it's potentially more future-proof than code."
+  ;; TODO: Add CHANNEL's introduction.  Currently we can't do that because
+  ;; older 'guix system describe' expect exactly name/url/branch/commit
+  ;; without any additional fields.
+  `(channel (name ,(channel-name channel))
+            (url ,(channel-url channel))
+            (branch ,(channel-branch channel))
+            (commit ,(channel-commit channel))))
+
+(define (sexp->channel sexp)
+  "Return the channel corresponding to SEXP, an sexp as found in the
+\"provenance\" file produced by 'provenance-service-type'."
+  (match sexp
+    (('channel ('name name)
+               ('url url)
+               ('branch branch)
+               ('commit commit)
+               rest ...)
+     ;; XXX: In the future REST may include a channel introduction.
+     (channel (name name) (url url)
+              (branch branch) (commit commit)))))
+
+(define (provenance-file channels config-file)
+  "Return a 'provenance' file describing CHANNELS, a list of channels, and
+CONFIG-FILE, which can be either #f or a <local-file> containing the OS
+configuration being used."
+  (scheme-file "provenance"
+               #~(provenance
+                  (version 0)
+                  (channels #+@(if channels
+                                   (map channel->sexp channels)
+                                   '()))
+                  (configuration-file #+config-file))))
+
+(define (provenance-entry config-file)
+  "Return system entries describing the operating system provenance: the
+channels in use and CONFIG-FILE, if it is true."
+  (define profile
+    (current-profile))
+
+  (define channels
+    (and=> profile profile-channels))
+
+  (mbegin %store-monad
+    (let ((config-file (cond ((string? config-file)
+                              ;; CONFIG-FILE has been passed typically via
+                              ;; 'guix system reconfigure CONFIG-FILE' so we
+                              ;; can assume it's valid: tell 'local-file' to
+                              ;; not emit a warning.
+                              (local-file (assume-valid-file-name config-file)
+                                          "configuration.scm"))
+                             ((not config-file)
+                              #f)
+                             (else
+                              config-file))))
+      (return `(("provenance" ,(provenance-file channels config-file))
+                ,@(if channels
+                      `(("channels.scm"
+                         ,(plain-file "channels.scm"
+                                      (object->pretty-string
+                                       `(list
+                                         ,@(map channel->code channels))))))
+                      '())
+                ,@(if config-file
+                      `(("configuration.scm" ,config-file))
+                      '()))))))
+
+(define provenance-service-type
+  (service-type (name 'provenance)
+                (extensions
+                 (list (service-extension system-service-type
+                                          provenance-entry)))
+                (default-value #f)                ;the OS config file
+                (description
+                 "Store provenance information about the system in the system
+itself: the channels used when building the system, and its configuration
+file, when available.")))
+
+(define (sexp->system-provenance sexp)
+  "Parse SEXP, an s-expression read from /run/current-system/provenance or
+similar, and return two values: the list of channels listed therein, and the
+OS configuration file or #f."
+  (match sexp
+    (('provenance ('version 0)
+                  ('channels channels ...)
+                  ('configuration-file config-file))
+     (values (map sexp->channel channels)
+             config-file))
+    (_
+     (values '() #f))))
+
+(define (system-provenance system)
+  "Given SYSTEM, the file name of a system generation, return two values: the
+list of channels SYSTEM is built from, and its configuration file.  If that
+information is missing, return the empty list (for channels) and possibly
+#false (for the configuration file)."
+  (catch 'system-error
+    (lambda ()
+      (sexp->system-provenance
+       (call-with-input-file (string-append system "/provenance")
+         read)))
+    (lambda _
+      (values '() #f))))
+\f
+;;;
+;;; Cleanup.
+;;;
+
 (define (cleanup-gexp _)
-  "Return as a monadic value a gexp to clean up /tmp and similar places upon
-boot."
-  (with-monad %store-monad
-    (with-imported-modules '((guix build utils))
-      (return #~(begin
-                  (use-modules (guix build utils))
-
-                  ;; Clean out /tmp and /var/run.
-                  ;;
-                  ;; XXX This needs to happen before service activations, so it
-                  ;; has to be here, but this also implicitly assumes that /tmp
-                  ;; and /var/run are on the root partition.
-                  (letrec-syntax ((fail-safe (syntax-rules ()
-                                               ((_ exp rest ...)
-                                                (begin
-                                                  (catch 'system-error
-                                                    (lambda () exp)
-                                                    (const #f))
-                                                  (fail-safe rest ...)))
-                                               ((_)
-                                                #t))))
-                    ;; Ignore I/O errors so the system can boot.
-                    (fail-safe
-                     ;; Remove stale Shadow lock files as they would lead to
-                     ;; failures of 'useradd' & co.
-                     (delete-file "/etc/group.lock")
-                     (delete-file "/etc/passwd.lock")
-                     (delete-file "/etc/.pwd.lock") ;from 'lckpwdf'
-
-                     (delete-file-recursively "/tmp")
-                     (delete-file-recursively "/var/run")
-                     (mkdir "/tmp")
-                     (chmod "/tmp" #o1777)
-                     (mkdir "/var/run")
-                     (chmod "/var/run" #o755))))))))
+  "Return a gexp to clean up /tmp and similar places upon boot."
+  (with-imported-modules '((guix build utils))
+    #~(begin
+        (use-modules (guix build utils))
+
+        ;; Clean out /tmp and /var/run.
+        ;;
+        ;; XXX This needs to happen before service activations, so it
+        ;; has to be here, but this also implicitly assumes that /tmp
+        ;; and /var/run are on the root partition.
+        (letrec-syntax ((fail-safe (syntax-rules ()
+                                     ((_ exp rest ...)
+                                      (begin
+                                        (catch 'system-error
+                                          (lambda () exp)
+                                          (const #f))
+                                        (fail-safe rest ...)))
+                                     ((_)
+                                      #t))))
+          ;; Ignore I/O errors so the system can boot.
+          (fail-safe
+           ;; Remove stale Shadow lock files as they would lead to
+           ;; failures of 'useradd' & co.
+           (delete-file "/etc/group.lock")
+           (delete-file "/etc/passwd.lock")
+           (delete-file "/etc/.pwd.lock")         ;from 'lckpwdf'
+
+           ;; Force file names to be decoded as UTF-8.  See
+           ;; <https://bugs.gnu.org/26353>.
+           (setenv "GUIX_LOCPATH"
+                   #+(file-append glibc-utf8-locales "/lib/locale"))
+           (setlocale LC_CTYPE "en_US.utf8")
+           (delete-file-recursively "/tmp")
+           (delete-file-recursively "/var/run")
+
+           (mkdir "/tmp")
+           (chmod "/tmp" #o1777)
+           (mkdir "/var/run")
+           (chmod "/var/run" #o755)
+           (delete-file-recursively "/run/udev/watch.old"))))))
 
 (define cleanup-service-type
   ;; Service that cleans things up in /tmp and similar.
   (service-type (name 'cleanup)
                 (extensions
                  (list (service-extension boot-service-type
-                                          cleanup-gexp)))))
+                                          cleanup-gexp)))
+                (description
+                 "Delete files from @file{/tmp}, @file{/var/run}, and other
+temporary locations at boot time.")))
 
 (define* (activation-service->script service)
   "Return as a monadic value the activation script for SERVICE, a service of
@@ -408,14 +594,10 @@ ACTIVATION-SCRIPT-TYPE."
 
 (define (activation-script gexps)
   "Return the system's activation script, which evaluates GEXPS."
-  (define (service-activations)
-    ;; Return the activation scripts for SERVICES.
-    (mapm %store-monad
-          (cut gexp->file "activate-service" <>)
-          gexps))
-
-  (mlet* %store-monad ((actions (service-activations)))
-    (gexp->file "activate"
+  (define actions
+    (map (cut program-file "activate-service.scm" <>) gexps))
+
+  (program-file "activate.scm"
                 (with-imported-modules (source-module-closure
                                         '((gnu build activation)
                                           (guix build utils)))
@@ -440,12 +622,17 @@ ACTIVATION-SCRIPT-TYPE."
 
                       ;; Run the services' activation snippets.
                       ;; TODO: Use 'load-compiled'.
-                      (for-each primitive-load '#$actions))))))
+                      (for-each primitive-load '#$actions)))))
 
 (define (gexps->activation-gexp gexps)
   "Return a gexp that runs the activation script containing GEXPS."
-  (mlet %store-monad ((script (activation-script gexps)))
-    (return #~(primitive-load #$script))))
+  #~(primitive-load #$(activation-script gexps)))
+
+(define (activation-profile-entry gexps)
+  "Return, as a monadic value, an entry for the activation script in the
+system directory."
+  (mlet %store-monad ((activate (lower-object (activation-script gexps))))
+    (return `(("activate" ,activate)))))
 
 (define (second-argument a b) b)
 
@@ -453,9 +640,14 @@ ACTIVATION-SCRIPT-TYPE."
   (service-type (name 'activate)
                 (extensions
                  (list (service-extension boot-service-type
-                                          gexps->activation-gexp)))
-                (compose append)
-                (extend second-argument)))
+                                          gexps->activation-gexp)
+                       (service-extension system-service-type
+                                          activation-profile-entry)))
+                (compose identity)
+                (extend second-argument)
+                (description
+                 "Run @dfn{activation} code at boot time and upon
+@command{guix system reconfigure} completion.")))
 
 (define %activation-service
   ;; The activation service produces the activation script from the gexps it
@@ -473,6 +665,10 @@ ACTIVATION-SCRIPT-TYPE."
                   #~(begin
                       (setenv "LINUX_MODULE_DIRECTORY"
                               "/run/booted-system/kernel/lib/modules")
+                      ;; FIXME: Remove this crutch when the patch #40422,
+                      ;; updating to kmod 27 is merged.
+                      (setenv "MODPROBE_OPTIONS"
+                              "-C /etc/modprobe.d")
                       (apply execl #$modprobe
                              (cons #$modprobe (cdr (command-line))))))))
 
@@ -493,6 +689,39 @@ ACTIVATION-SCRIPT-TYPE."
                   activation-service-type
                   %linux-kernel-activation))
 
+(define %hurd-rc-script
+  ;; The RC script to be started upon boot.
+  (program-file "rc"
+                (with-imported-modules (source-module-closure
+                                        '((guix build utils)
+                                          (gnu build hurd-boot)
+                                          (guix build syscalls)))
+                  #~(begin
+                      (use-modules (guix build utils)
+                                   (gnu build hurd-boot)
+                                   (guix build syscalls)
+                                   (ice-9 match)
+                                   (system repl repl)
+                                   (srfi srfi-1)
+                                   (srfi srfi-26))
+                      (boot-hurd-system)))))
+
+(define (hurd-rc-entry rc)
+  "Return, as a monadic value, an entry for the RC script in the system
+directory."
+  (mlet %store-monad ((rc (lower-object rc)))
+    (return `(("rc" ,rc)))))
+
+(define hurd-startup-service-type
+  ;; The service that creates the initial SYSTEM/rc startup file.
+  (service-type (name 'startup)
+                (extensions
+                 (list (service-extension system-service-type hurd-rc-entry)))
+                (default-value %hurd-rc-script)))
+
+(define %hurd-startup-service
+  ;; The service that produces the RC script.
+  (service hurd-startup-service-type %hurd-rc-script))
 
 (define special-files-service-type
   ;; Service to install "special files" such as /bin/sh and /usr/bin/env.
@@ -503,7 +732,10 @@ ACTIVATION-SCRIPT-TYPE."
                              (lambda (files)
                                #~(activate-special-files '#$files)))))
    (compose concatenate)
-   (extend append)))
+   (extend append)
+   (description
+    "Add special files to the root file system---e.g.,
+@file{/usr/bin/env}.")))
 
 (define (extra-special-file file target)
   "Use TARGET as the \"special file\" FILE.  For example, TARGET might be
@@ -518,6 +750,21 @@ and FILE could be \"/usr/bin/env\"."
   (files->etc-directory (service-value service)))
 
 (define (files->etc-directory files)
+  (define (assert-no-duplicates files)
+    (let loop ((files files)
+               (seen (set)))
+      (match files
+        (() #t)
+        (((file _) rest ...)
+         (when (set-contains? seen file)
+           (raise (formatted-message (G_ "duplicate '~a' entry for /etc")
+                                     file)))
+         (loop rest (set-insert file seen))))))
+
+  ;; Detect duplicates early instead of letting them through, eventually
+  ;; leading to a build failure of "etc.drv".
+  (assert-no-duplicates files)
+
   (file-union "etc" files))
 
 (define (etc-entry files)
@@ -537,7 +784,8 @@ directory."
                                          #~(activate-etc #$etc))))
                   (service-extension system-service-type etc-entry)))
                 (compose concatenate)
-                (extend append)))
+                (extend append)
+                (description "Populate the @file{/etc} directory.")))
 
 (define (etc-service files)
   "Return a new service of ETC-SERVICE-TYPE that populates /etc with FILES.
@@ -552,14 +800,23 @@ FILES must be a list of name/file-like object pairs."
                                             #~(activate-setuid-programs
                                                (list #$@programs))))))
                 (compose concatenate)
-                (extend append)))
+                (extend append)
+                (description
+                 "Populate @file{/run/setuid-programs} with the specified
+executables, making them setuid-root.")))
 
 (define (packages->profile-entry packages)
   "Return a system entry for the profile containing PACKAGES."
-  (mlet %store-monad ((profile (profile-derivation
-                                (packages->manifest
-                                 (delete-duplicates packages eq?)))))
-    (return `(("profile" ,profile)))))
+  ;; XXX: 'mlet' is needed here for one reason: to get the proper
+  ;; '%current-target' and '%current-target-system' bindings when
+  ;; 'packages->manifest' is called, and thus when the 'package-inputs'
+  ;; etc. procedures are called on PACKAGES.  That way, conditionals in those
+  ;; inputs see the "correct" value of these two parameters.  See
+  ;; <https://issues.guix.gnu.org/44952>.
+  (mlet %store-monad ((_ (current-target-system)))
+    (return `(("profile" ,(profile
+                           (content (packages->manifest
+                                     (delete-duplicates packages eq?)))))))))
 
 (define profile-service-type
   ;; The service that populates the system's profile---i.e.,
@@ -569,7 +826,11 @@ FILES must be a list of name/file-like object pairs."
                  (list (service-extension system-service-type
                                           packages->profile-entry)))
                 (compose concatenate)
-                (extend append)))
+                (extend append)
+                (description
+                 "This is the @dfn{system profile}, available as
+@file{/run/current-system/profile}.  It contains packages that the sysadmin
+wants to be globally available to all the system users.")))
 
 (define (firmware->activation-gexp firmware)
   "Return a gexp to make the packages listed in FIRMWARE loadable by the
@@ -585,7 +846,11 @@ kernel."
                  (list (service-extension activation-service-type
                                           firmware->activation-gexp)))
                 (compose concatenate)
-                (extend append)))
+                (extend append)
+                (description
+                 "Make ``firmware'' files loadable by the operating system
+kernel.  Firmware may then be uploaded to some of the machine's devices, such
+as Wifi cards.")))
 
 (define (gc-roots->system-entry roots)
   "Return an entry in the system's output containing symlinks to ROOTS."
@@ -612,7 +877,11 @@ kernel."
                  (list (service-extension system-service-type
                                           gc-roots->system-entry)))
                 (compose concatenate)
-                (extend append)))
+                (extend append)
+                (description
+                 "Register garbage-collector roots---i.e., store items that
+will not be reclaimed by the garbage collector.")
+                (default-value '())))
 
 \f
 ;;;
@@ -629,6 +898,18 @@ kernel."
   (service      ambiguous-target-service-error-service)
   (target-type  ambiguous-target-service-error-target-type))
 
+(define (missing-target-error service target-type)
+  (raise
+   (condition (&missing-target-service-error
+               (service service)
+               (target-type target-type))
+              (&message
+               (message
+                (format #f (G_ "no target of type '~a' for service '~a'")
+                        (service-type-name target-type)
+                        (service-type-name
+                         (service-kind service))))))))
+
 (define (service-back-edges services)
   "Return a procedure that, when passed a <service>, returns the list of
 <service> objects that depend on it."
@@ -641,16 +922,7 @@ kernel."
           ((target)
            (vhash-consq target service edges))
           (()
-           (raise
-            (condition (&missing-target-service-error
-                        (service service)
-                        (target-type target-type))
-                       (&message
-                        (message
-                         (format #f (G_ "no target of type '~a' for service '~a'")
-                                 (service-type-name target-type)
-                                 (service-type-name
-                                  (service-kind service))))))))
+           (missing-target-error service target-type))
           (x
            (raise
             (condition (&ambiguous-target-service-error
@@ -668,6 +940,48 @@ kernel."
     (lambda (node)
       (reverse (vhash-foldq* cons '() node edges)))))
 
+(define (instantiate-missing-services services)
+  "Return SERVICES, a list, augmented with any services targeted by extensions
+and missing from SERVICES.  Only service types with a default value can be
+instantiated; other missing services lead to a
+'&missing-target-service-error'."
+  (define (adjust-service-list svc result instances)
+    (fold2 (lambda (extension result instances)
+             (define target-type
+               (service-extension-target extension))
+
+             (match (vhash-assq target-type instances)
+               (#f
+                (let ((default (service-type-default-value target-type)))
+                  (if (eq? &no-default-value default)
+                      (missing-target-error svc target-type)
+                      (let ((new (service target-type)))
+                        (values (cons new result)
+                                (vhash-consq target-type new instances))))))
+               (_
+                (values result instances))))
+           result
+           instances
+           (service-type-extensions (service-kind svc))))
+
+  (let loop ((services services))
+    (define instances
+      (fold (lambda (service result)
+              (vhash-consq (service-kind service) service
+                           result))
+            vlist-null services))
+
+    (define adjusted
+      (fold2 adjust-service-list
+             services instances
+             services))
+
+    ;; If we instantiated services, they might in turn depend on missing
+    ;; services.  Loop until we've reached fixed point.
+    (if (= (length adjusted) (vlist-length instances))
+        adjusted
+        (loop adjusted))))
+
 (define* (fold-services services
                         #:key (target-type system-service-type))
   "Fold SERVICES by propagating their extensions down to the root of type
@@ -692,26 +1006,42 @@ TARGET-TYPE; return the root service adjusted accordingly."
                    (eq? (service-kind service) target-type))
                  services)
     ((sink)
-     (let loop ((sink sink))
-       (let* ((dependents (map loop (dependents sink)))
-              (extensions (map (apply-extension sink) dependents))
-              (extend     (service-type-extend (service-kind sink)))
-              (compose    (service-type-compose (service-kind sink)))
-              (params     (service-value sink)))
-         ;; We distinguish COMPOSE and EXTEND because PARAMS typically has a
-         ;; different type than the elements of EXTENSIONS.
-         (if extend
-             (service (service-kind sink)
-                      (extend params (compose extensions)))
-             sink))))
+     ;; Use the state monad to keep track of already-visited services in the
+     ;; graph and to memoize their value once folded.
+     (run-with-state
+         (let loop ((sink sink))
+           (mlet %state-monad ((visited (current-state)))
+             (match (vhash-assq sink visited)
+               (#f
+                (mlet* %state-monad
+                    ((dependents (mapm %state-monad loop (dependents sink)))
+                     (visited    (current-state))
+                     (extensions -> (map (apply-extension sink) dependents))
+                     (extend     -> (service-type-extend (service-kind sink)))
+                     (compose    -> (service-type-compose (service-kind sink)))
+                     (params     -> (service-value sink))
+                     (service
+                      ->
+                      ;; Distinguish COMPOSE and EXTEND because PARAMS typically
+                      ;; has a different type than the elements of EXTENSIONS.
+                      (if extend
+                          (service (service-kind sink)
+                                   (extend params (compose extensions)))
+                          sink)))
+                  (mbegin %state-monad
+                    (set-current-state (vhash-consq sink service visited))
+                    (return service))))
+               ((_ . service)                     ;SINK was already visited
+                (return service)))))
+       vlist-null))
     (()
      (raise
-      (condition (&missing-target-service-error
-                  (service #f)
-                  (target-type target-type))
-                 (&message
-                  (message (format #f (G_ "service of type '~a' not found")
-                                   (service-type-name target-type)))))))
+      (make-compound-condition
+       (condition (&missing-target-service-error
+                   (service #f)
+                   (target-type target-type)))
+       (formatted-message (G_ "service of type '~a' not found")
+                          (service-type-name target-type)))))
     (x
      (raise
       (condition (&ambiguous-target-service-error