gnu: surgescript: Update to 0.5.4.4.
[jackhill/guix/guix.git] / gnu / services.scm
index 5c31474..11ba21e 100644 (file)
@@ -1,6 +1,7 @@
 ;;; 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>
 ;;;
 ;;; This file is part of GNU Guix.
 ;;;
   #:use-module (guix store)
   #: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)
@@ -37,6 +44,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
             service-type-compose
             service-type-extend
             service-type-default-value
+            service-type-description
+            service-type-location
+
+            %service-type-path
+            fold-service-types
+            lookup-service-types
 
             service
             service?
@@ -59,6 +73,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
-
-            file-union))                      ;XXX: for lack of a better place
+            etc-service))
 
 ;;; Comment:
 ;;;
 
   ;; Optional default value for instances of this type.
   (default-value service-type-default-value       ;Any
-                 (default &no-default-value)))
+                 (default &no-default-value))
+
+  ;; Meta-data.
+  (description  service-type-description          ;string
+                (default #f))
+  (location     service-type-location             ;<location>
+                (default (and=> (current-source-location)
+                                source-properties->location))
+                (innate)))
 
 (define (write-service-type type port)
   (format port "#<service-type ~a ~a>"
 
 (set-record-type-printer! <service-type> write-service-type)
 
+(define %distro-root-directory
+  ;; Absolute file name of the module hierarchy.
+  (dirname (search-path %load-path "guix.scm")))
+
+(define %service-type-path
+  ;; Search path for service types.
+  (make-parameter `((,%distro-root-directory . "gnu/services")
+                    (,%distro-root-directory . "gnu/system"))))
+
+(define (all-service-modules)
+  "Return the default set of service modules."
+  (cons (resolve-interface '(gnu services))
+        (all-modules (%service-type-path)
+                     #:warn warn-about-load-error)))
+
+(define* (fold-service-types proc seed
+                             #:optional
+                             (modules (all-service-modules)))
+  "For each service type exported by one of MODULES, call (PROC RESULT).  SEED
+is used as the initial value of RESULT."
+  (fold-module-public-variables (lambda (object result)
+                                  (if (service-type? object)
+                                      (proc object result)
+                                      result))
+                                seed
+                                modules))
+
+(define lookup-service-types
+  (let ((table
+         (delay (fold-service-types (lambda (type result)
+                                      (vhash-consq (service-type-name type)
+                                                   type result))
+                                    vlist-null))))
+    (lambda (name)
+      "Return the list of services with the given NAME (a symbol)."
+      (vhash-foldq* cons '() name (force table)))))
+
 ;; Services of a given type.
 (define-record-type <service>
   (make-service type value)
@@ -180,13 +243,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
@@ -262,11 +325,11 @@ This is a shorthand for (map (lambda (svc) ...) %base-services)."
 ;;; 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))))))
@@ -279,13 +342,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
@@ -294,89 +364,212 @@ 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)
+                              (local-file 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
-                     (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)))))
-
-(define* (file-union name files)                  ;FIXME: Factorize.
-  "Return a <computed-file> that builds a directory containing all of FILES.
-Each item in FILES must be a list where the first element is the file name to
-use in the new directory, and the second element is a gexp denoting the target
-file."
-  (computed-file name
-                 #~(begin
-                     (mkdir #$output)
-                     (chdir #$output)
-                     #$@(map (match-lambda
-                               ((target source)
-                                #~(begin
-                                    ;; Stat the source to abort early if it
-                                    ;; does not exist.
-                                    (stat #$source)
-
-                                    (symlink #$source #$target))))
-                             files))))
-
-(define (directory-union name things)
-  "Return a directory that is the union of THINGS."
-  (match things
-    ((one)
-     ;; Only one thing; return it.
-     one)
-    (_
-     (computed-file name
-                    (with-imported-modules '((guix build union))
-                      #~(begin
-                          (use-modules (guix build union))
-                          (union-build #$output '#$things)))))))
+                                          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
@@ -385,14 +578,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)))
@@ -417,12 +606,11 @@ 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 (second-argument a b) b)
 
@@ -431,8 +619,11 @@ ACTIVATION-SCRIPT-TYPE."
                 (extensions
                  (list (service-extension boot-service-type
                                           gexps->activation-gexp)))
-                (compose append)
-                (extend second-argument)))
+                (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
@@ -450,6 +641,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))))))))
 
@@ -470,6 +665,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.
@@ -480,7 +708,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
@@ -495,6 +726,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)
@@ -514,7 +760,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.
@@ -529,14 +776,17 @@ 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)))))
+  (with-monad %store-monad
+    (return `(("profile" ,(profile
+                           (content (packages->manifest
+                                     (delete-duplicates packages eq?)))))))))
 
 (define profile-service-type
   ;; The service that populates the system's profile---i.e.,
@@ -546,7 +796,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
@@ -562,7 +816,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."
@@ -589,7 +847,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
 ;;;
@@ -606,6 +868,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."
@@ -618,15 +892,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 ~s")
-                                 (service-type-name target-type)
-                                 service))))))
+           (missing-target-error service target-type))
           (x
            (raise
             (condition (&ambiguous-target-service-error
@@ -644,6 +910,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
@@ -668,26 +976,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