;;; GNU Guix --- Functional package management for GNU
;;; Copyright © 2018, 2019 Mathieu Othacehe <m.othacehe@gmail.com>
;;; Copyright © 2020-2022 Ludovic Courtès <ludo@gnu.org>
;;; Copyright © 2024 Janneke Nieuwenhuizen <janneke@gnu.org>
;;;
;;; 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 installer steps)
  #:use-module (guix records)
  #:use-module (guix build utils)
  #:use-module (guix i18n)
  #:use-module (guix read-print)
  #:use-module (guix utils)
  #:use-module (gnu installer utils)
  #:use-module (ice-9 match)
  #:use-module (srfi srfi-1)
  #:use-module (srfi srfi-34)
  #:use-module (srfi srfi-35)
  #:use-module (rnrs io ports)
  #:export (&user-abort-error
            user-abort-error?

            <installer-step>
            installer-step
            make-installer-step
            installer-step?
            installer-step-id
            installer-step-description
            installer-step-compute
            installer-step-configuration-formatter

            run-installer-steps
            find-step-by-id
            result->step-ids
            result-step
            result-step-done?

            %installer-configuration-file
            %installer-target-dir
            format-configuration
            configuration->file

            %current-result))

(define-condition-type &user-abort-error &error
  user-abort-error?)

;; Hash table storing the step results. Use it only for logging and debug
;; purposes.
(define %current-result (make-hash-table))

;; An installer-step record is basically an id associated to a compute
;; procedure. The COMPUTE procedure takes exactly one argument, an association
;; list containing the results of previously executed installer-steps (see
;; RUN-INSTALLER-STEPS description). The value returned by the COMPUTE
;; procedure will be stored in the results list passed to the next
;; installer-step and so on.
(define-record-type* <installer-step>
  installer-step make-installer-step
  installer-step?
  (id                         installer-step-id) ;symbol
  (description                installer-step-description ;string
                              (default #f)

                              ;; Make it thunked so that 'G_' is called at the
                              ;; right time, as opposed to being called once
                              ;; when the installer starts.
                              (thunked))
  (compute                    installer-step-compute) ;procedure
  (configuration-formatter    installer-step-configuration-formatter ;procedure
                              (default #f)))

(define* (run-installer-steps #:key
                              steps
                              (rewind-strategy 'previous)
                              (menu-proc (const #f))
                              dry-run?)
  "Run the COMPUTE procedure of all <installer-step> records in STEPS
sequentially, inside a the 'installer-step prompt.  When aborted to with a
parameter of 'abort, fallback to a previous install-step, accordingly to the
specified REWIND-STRATEGY.  When aborted to with a parameter of 'break, stop
the computation and return the accumalated result so far.

REWIND-STRATEGY possible values are 'previous, 'menu and 'start.  If 'previous
is selected, the execution will resume at the previous installer-step. If
'menu is selected, the MENU-PROC procedure will be called. Its return value
has to be an installer-step ID to jump to. The ID has to be the one of a
previously executed step. It is impossible to jump forward. Finally if 'start
is selected, the execution will resume at the first installer-step.

The result of every COMPUTE procedures is stored in an association list, under
the form:

		'((STEP-ID . COMPUTE-RESULT) ...)

where STEP-ID is the ID field of the installer-step and COMPUTE-RESULT the
result of the associated COMPUTE procedure. This result association list is
passed as argument of every COMPUTE procedure. It is finally returned when the
computation is over."
  (define (pop-result list)
    (cdr list))

  (define (first-step? steps step)
    (match steps
      ((first-step . rest-steps)
       (equal? first-step step))))

  (define* (skip-to-step step result
                         #:key todo-steps done-steps)
    (match todo-steps
      ((todo . rest-todo)
       (let ((found? (eq? (installer-step-id todo)
                          (installer-step-id step))))
         (cond
          (found?
           (run result
                #:todo-steps todo-steps
                #:done-steps done-steps))
          ((and (not found?)
                (null? done-steps))
           (error (format #f "Step ~a not found" (installer-step-id step))))
          (else
           (match done-steps
             ((prev-done ... last-done)
              (skip-to-step step (pop-result result)
                            #:todo-steps (cons last-done todo-steps)
                            #:done-steps prev-done)))))))))

  (define* (run result #:key todo-steps done-steps)
    (match todo-steps
      (() (reverse result))
      ((step . rest-steps)
       (call-with-prompt 'installer-step
         (lambda ()
           (installer-log-line "running step '~a'" (installer-step-id step))
           (let* ((id (installer-step-id step))
                  (compute (installer-step-compute step))
                  (res (compute result done-steps)))
             (hash-set! %current-result id res)
             (run (alist-cons id res result)
                  #:todo-steps rest-steps
                  #:done-steps (append done-steps (list step)))))
         (lambda (k action)
           (match action
             ('abort
              (case rewind-strategy
                ((previous)
                 (match done-steps
                   (()
                    ;; We cannot go previous the first step. Abort again to
                    ;; 'installer-step prompt. It might be useful in the case
                    ;; of nested run-installer-steps.
                    (abort-to-prompt 'installer-step action))
                   ((prev-done ... last-done)
                    (run (pop-result result)
                         #:todo-steps (cons last-done todo-steps)
                         #:done-steps prev-done))))
                ((menu)
                 (let ((goto-step (menu-proc
                                   (append done-steps (list step)))))
                   (if (eq? goto-step step)
                       (run result
                            #:todo-steps todo-steps
                            #:done-steps done-steps)
                       (skip-to-step goto-step result
                                     #:todo-steps todo-steps
                                     #:done-steps done-steps))))
                ((start)
                 (if (null? done-steps)
                     ;; Same as above, it makes no sense to jump to start
                     ;; when we are at the first installer-step. Abort to
                     ;; 'installer-step prompt again.
                     (abort-to-prompt 'installer-step action)
                     (run '()
                          #:todo-steps steps
                          #:done-steps '())))))
             ('break
              (reverse result))))))))

  ;; Ignore SIGPIPE so that we don't die if a client closes the connection
  ;; prematurely.
  (sigaction SIGPIPE SIG_IGN)

  (if dry-run?
      (run '()
           #:todo-steps steps
           #:done-steps '())
      (with-server-socket
        (run '()
             #:todo-steps steps
             #:done-steps '()))))

(define (find-step-by-id steps id)
  "Find and return the step in STEPS whose id is equal to ID."
  (find (lambda (step)
          (eq? (installer-step-id step) id))
        steps))

(define (result-step results step-id)
  "Return the result of the installer-step specified by STEP-ID in
RESULTS."
  (assoc-ref results step-id))

(define (result-step-done? results step-id)
  "Return #t if the installer-step specified by STEP-ID has a COMPUTE value
stored in RESULTS. Return #f otherwise."
  (and (assoc step-id results) #t))

(define %installer-configuration-file (make-parameter "/mnt/etc/config.scm"))
(define %installer-target-dir (make-parameter "/mnt"))

(define (format-configuration steps results)
  "Return the list resulting from the application of the procedure defined in
CONFIGURATION-FORMATTER field of <installer-step> on the associated result
found in RESULTS."
  (let ((configuration
         (append-map
          (lambda (step)
            (let* ((step-id (installer-step-id step))
                   (conf-formatter
                    (installer-step-configuration-formatter step))
                   (result-step (result-step results step-id)))
              (if (and result-step conf-formatter)
                  (conf-formatter result-step)
                  '())))
          steps))
        (modules `(,(vertical-space 1)
                   ,(comment (G_ "\
;; Indicate which modules to import to access the variables
;; used in this configuration.\n"))
                   ,@(if (target-hurd?)
                         '((use-modules (gnu) (gnu system hurd))
                           (use-package-modules hurd ssh))
                         '((use-modules (gnu))))
                   (use-service-modules cups desktop networking ssh xorg))))
    `(,@modules
      ,(vertical-space 1)
      (operating-system ,@configuration))))

(define* (configuration->file configuration
                              #:key (file-name (%installer-configuration-file)))
  "Write the given CONFIGURATION to FILE-NAME."
  (mkdir-p (dirname file-name))
  (call-with-output-file file-name
    (lambda (port)
      ;; TRANSLATORS: This is a comment within a Scheme file.  Each line must
      ;; start with ";; " (two semicolons and a space).  Please keep line
      ;; length below 60 characters.
      (display (G_ "\
;; This is an operating system configuration generated
;; by the graphical installer.
;;
;; Once installation is complete, you can learn and modify
;; this file to tweak the system configuration, and pass it
;; to the 'guix system reconfigure' command to effect your
;; changes.\n")
               port)
      (newline port)
      (pretty-print-with-comments/splice port configuration
                                         #:max-width 75
                                         #:format-comment
                                         (lambda (c indent)
                                           ;; Localize C.
                                           (comment (G_ (comment->string c))
                                                    (comment-margin? c))))

      (flush-output-port port))))

;;; Local Variables:
;;; eval: (put 'with-server-socket 'scheme-indent-function 0)
;;; End:
information." ((_ (delete kind) rest ...) (cons (list kind (lambda (service) #f) (current-source-location)) (clause-alist rest ...))) ((_ (kind param => exp ...) rest ...) (cons (list kind (lambda (svc) (let ((param (service-value svc))) (service (service-kind svc) (begin exp ...)))) (current-source-location)) (clause-alist rest ...))) ((_) '()))) (define (apply-clauses clauses service deleted-services) "Apply CLAUSES, an alist as returned by 'clause-alist', to SERVICE. An exception is raised if a clause attempts to modify a service present in DELETED-SERVICES." (define (raise-if-deleted kind properties) (match (find (match-lambda ((deleted-kind _) (eq? kind deleted-kind))) deleted-services) ((_ deleted-properties) (raise (make-compound-condition (condition (&error-location (location (source-properties->location properties)))) (formatted-message (G_ "modify-services: service '~a' was deleted here: ~a") (service-type-name kind) (source-properties->location deleted-properties))))) (_ #t))) (match clauses (((kind proc properties) . rest) (raise-if-deleted kind properties) (if (eq? (and service (service-kind service)) kind) (let ((new-service (proc service))) (apply-clauses rest new-service (if new-service deleted-services (cons (list kind properties) deleted-services)))) (apply-clauses rest service deleted-services))) (() service))) (define (%modify-services services clauses) "Apply CLAUSES, an alist as returned by 'clause-alist', to SERVICES. An exception is raised if a clause attempts to modify a missing service." (define (raise-if-not-found clause) (match clause ((kind _ properties) (unless (find (lambda (service) (eq? kind (service-kind service))) services) (raise (make-compound-condition (condition (&error-location (location (source-properties->location properties)))) (formatted-message (G_ "modify-services: service '~a' not found in service list") (service-type-name kind)))))))) (for-each raise-if-not-found clauses) (reverse (filter-map identity (fold (lambda (service services) (cons (apply-clauses clauses service '()) services)) '() services)))) (define-syntax modify-services (syntax-rules () "Modify the services listed in SERVICES according to CLAUSES and return the resulting list of services. Each clause must have the form: (TYPE VARIABLE => BODY) where TYPE is a service type, such as 'guix-service-type', and VARIABLE is an identifier that is bound within BODY to the value of the service of that TYPE. Clauses can also remove services of a given type: (delete TYPE) Consider this example: (modify-services %base-services (guix-service-type config => (guix-configuration (inherit config) (use-substitutes? #f) (extra-options '(\"--gc-keep-derivations\")))) (mingetty-service-type config => (mingetty-configuration (inherit config) (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, and it deletes instances of the UDEV-SERVICE-TYPE." ((_ services clauses ...) (%modify-services services (clause-alist clauses ...))))) ;;; ;;; Core services. ;;; (define (system-derivation entries mextensions) "Return as a monadic value the derivation of the 'system' directory containing the given entries." (mlet %store-monad ((extensions (mapm/accumulate-builds identity mextensions))) (lower-object (file-union "system" (append entries (concatenate extensions)))))) (define system-service-type ;; This is the ultimate service type, the root of the service DAG. The ;; service of this type is extended by monadic name/item pairs. These items ;; end up in the "system directory" as returned by ;; 'operating-system-derivation'. (service-type (name 'system) (extensions '()) (compose identity) (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 directory." (mlet %store-monad ((boot mboot)) (return `(("boot" ,boot))))) (define boot-service-type ;; 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 identity) (extend compute-boot-script) (default-value #f) (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)) ;;; ;;; 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 channels (current-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)))) ;;; ;;; Cleanup. ;;; (define (cleanup-gexp _) "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, /var/run, and /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 (libc-utf8-locales-for-target (%current-system)) "/lib/locale")) (setlocale LC_CTYPE "en_US.utf8") (delete-file-recursively "/tmp") (delete-file-recursively "/var/run") (delete-file-recursively "/run") ;; Note: The second argument to 'mkdir' is and'ed with umask, ;; hence the 'chmod' calls. (mkdir "/tmp" #o1777) (chmod "/tmp" #o1777) (mkdir "/var/run" #o755) (chmod "/var/run" #o755) (mkdir "/run" #o755) (chmod "/var/run" #o755)))))) (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))) (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 ACTIVATION-SCRIPT-TYPE." (activation-script (service-value service))) (define (activation-script gexps) "Return the system's activation script, which evaluates GEXPS." (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))) #~(begin (use-modules (gnu build activation) (guix build utils)) (mkdir-p "/var/run") ;; Make sure the user accounting database exists. If it ;; does not exist, 'setutxent' does not create it and ;; thus there is no accounting at all. (close-port (open-file "/var/run/utmpx" "a0")) ;; Same for 'wtmp', which is populated by mingetty et ;; al. (mkdir-p "/var/log") (close-port (open-file "/var/log/wtmp" "a0")) ;; Set up /run/current-system. Among other things this ;; sets up locales, which the activation snippets ;; executed below may expect. (activate-current-system) ;; Run the services' activation snippets. ;; TODO: Use 'load-compiled'. (for-each primitive-load '#$actions))))) (define (gexps->activation-gexp gexps) "Return a gexp that runs the activation script containing GEXPS." #~(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) (define activation-service-type (service-type (name 'activate) (extensions (list (service-extension boot-service-type gexps->activation-gexp) (service-extension system-service-type activation-profile-entry))) (compose identity) (extend second-argument) (default-value #f) (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 ;; receives. (service activation-service-type #t)) (define %modprobe-wrapper ;; Wrapper for the 'modprobe' command that knows where modules live. ;; ;; This wrapper is typically invoked by the Linux kernel ('call_modprobe', ;; in kernel/kmod.c), a situation where the 'LINUX_MODULE_DIRECTORY' ;; environment variable is not set---hence the need for this wrapper. (let ((modprobe "/run/current-system/profile/bin/modprobe")) (program-file "modprobe" #~(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)))))))) (define %linux-kernel-activation ;; Activation of the Linux kernel running on the bare metal (as opposed to ;; running in a container.) #~(begin ;; Tell the kernel to use our 'modprobe' command. (activate-modprobe #$%modprobe-wrapper) ;; Let users debug their own processes! (activate-ptrace-attach))) (define %linux-bare-metal-service ;; The service that does things that are needed on the "bare metal", but not ;; necessary or impossible in a container. (simple-service 'linux-bare-metal 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) (description "This service creates an @file{rc} script in the system; that script is responsible for booting the Hurd."))) (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. (service-type (name 'special-files) (extensions (list (service-extension activation-service-type (lambda (files) #~(activate-special-files '#$files))))) (compose concatenate) (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 (file-append coreutils \"/bin/env\") and FILE could be \"/usr/bin/env\"." (simple-service (string->symbol (string-append "special-file-" file)) special-files-service-type `((,file ,target)))) (define (etc-directory service) "Return the directory for SERVICE, a service of type ETC-SERVICE-TYPE." (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) "Return an entry for the /etc directory consisting of FILES in the system directory." (with-monad %store-monad (return `(("etc" ,(files->etc-directory files)))))) (define etc-service-type (service-type (name 'etc) (extensions (list (service-extension activation-service-type (lambda (files) (let ((etc (files->etc-directory files))) #~(activate-etc #$etc)))) (service-extension system-service-type etc-entry))) (compose concatenate) (extend append) (default-value '()) (description "Populate the @file{/etc} directory."))) (define-deprecated (etc-service files) etc-service-type "Return a new service of ETC-SERVICE-TYPE that populates /etc with FILES. FILES must be a list of name/file-like object pairs." (service etc-service-type files)) (define (privileged-program->activation-gexp programs) "Return an activation gexp for privileged-program from PROGRAMS." (let ((programs (map (lambda (program) ;; FIXME This is really ugly, I didn't manage to use "inherit". (let ((program-name (privileged-program-program program)) (setuid? (privileged-program-setuid? program)) (setgid? (privileged-program-setgid? program)) (user (privileged-program-user program)) (group (privileged-program-group program)) (capabilities (privileged-program-capabilities program))) (unless (or setuid? setgid? capabilities) (warning (G_ "so-called privileged-program ~s lacks any privilege~%") program-name)) #~(privileged-program (setuid? #$setuid?) (setgid? #$setgid?) (user #$user) (group #$group) (capabilities #$capabilities) (program #$program-name)))) programs))) (with-imported-modules (source-module-closure '((gnu system privilege))) #~(begin (use-modules (gnu system privilege)) (let ((libcap #$(let-system (system target) ;; When cross-compiling, assume libcap is ;; available on GNU/Linux only. (and (if target (string-suffix? "linux-gnu" target) (supported-package? libcap system)) libcap)))) (activate-privileged-programs (list #$@programs) libcap)))))) (define privileged-program-service-type (service-type (name 'privileged-program) (extensions (list (service-extension activation-service-type privileged-program->activation-gexp))) (compose concatenate) (extend (lambda (config extensions) (append config extensions))) (description "Copy the specified executables to @file{/run/privileged/bin} and apply special privileges like setuid and/or setgid. The deprecated @file{/run/setuid-programs} directory is also populated with symbolic links to their @file{/run/privileged/bin} counterpart. It will be removed in a future Guix release."))) (define-deprecated/alias setuid-program-service-type ;; Deprecated alias to ease transition. Will be removed! privileged-program-service-type) (define (packages->profile-entry packages) "Return a system entry for the profile containing PACKAGES." ;; 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., ;; /run/current-system/profile. It is extended by package lists. (service-type (name 'profile) (extensions (list (service-extension system-service-type packages->profile-entry))) (compose concatenate) (extend append) (default-value '()) (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 kernel." (let ((directory (directory-union "firmware" firmware))) ;; Tell the kernel where firmware is. #~(activate-firmware (string-append #$directory "/lib/firmware")))) (define firmware-service-type ;; The service that collects firmware. (service-type (name 'firmware) (extensions (list (service-extension activation-service-type firmware->activation-gexp))) (compose concatenate) (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." (mlet %store-monad ((entry (gexp->derivation "gc-roots" #~(let ((roots '#$roots)) (mkdir #$output) (chdir #$output) (for-each symlink roots (map number->string (iota (length roots)))))))) (return (if (null? roots) '() `(("gc-roots" ,entry)))))) (define gc-root-service-type ;; A service to associate extra garbage-collector roots to the system. This ;; is a simple hack that guarantees that the system retains references to ;; the given list of roots. Roots must be "lowerable" objects like ;; packages, or derivations. (service-type (name 'gc-roots) (extensions (list (service-extension system-service-type gc-roots->system-entry))) (compose concatenate) (extend append) (description "Register garbage-collector roots---i.e., store items that will not be reclaimed by the garbage collector.") (default-value '()))) ;; Configuration for the Linux kernel builder. (define-record-type* <linux-builder-configuration> linux-builder-configuration make-linux-builder-configuration linux-builder-configuration? this-linux-builder-configuration (kernel linux-builder-configuration-kernel) ; package (modules linux-builder-configuration-modules (default '()))) ; list of packages (define (package-for-kernel target-kernel module-package) "Return a package like MODULE-PACKAGE, adapted for TARGET-KERNEL, if possible (that is if there's a LINUX keyword argument in the build system)." (package (inherit module-package) (arguments (substitute-keyword-arguments (package-arguments module-package) ((#:linux kernel #f) target-kernel))))) (define (linux-builder-configuration->system-entry config) "Return the kernel entry of the 'system' directory." (let* ((kernel (linux-builder-configuration-kernel config)) (modules (linux-builder-configuration-modules config)) (kernel (profile (content (packages->manifest (cons kernel (map (lambda (module) (cond ((package? module) (package-for-kernel kernel module)) ;; support (,package "kernel-module-output") ((and (list? module) (package? (car module))) (cons (package-for-kernel kernel (car module)) (cdr module))) (else module))) modules)))) (hooks (list linux-module-database))))) (with-monad %store-monad (return `(("kernel" ,kernel)))))) (define linux-builder-service-type (service-type (name 'linux-builder) (extensions (list (service-extension system-service-type linux-builder-configuration->system-entry))) (default-value '()) (compose identity) (extend (lambda (config modifiers) (if (null? modifiers) config ((apply compose modifiers) config)))) (description "Builds the linux-libre kernel profile, containing the kernel itself and any linux-loadable kernel modules. This can be extended with a function that accepts the current configuration and returns a new configuration."))) (define (linux-loadable-module-builder-modifier modules) "Extends linux-builder-service-type by appending the given MODULES to the configuration of linux-builder-service-type." (lambda (config) (linux-builder-configuration (inherit config) (modules (append (linux-builder-configuration-modules config) modules))))) (define linux-loadable-module-service-type (service-type (name 'linux-loadable-modules) (extensions (list (service-extension linux-builder-service-type linux-loadable-module-builder-modifier))) (default-value '()) (compose concatenate) (extend append) (description "Adds packages and package outputs as modules included in the booted linux-libre profile. Other services can extend this service type to add particular modules to the set of linux-loadable modules."))) ;;; ;;; Service folding. ;;; (define-condition-type &missing-target-service-error &service-error missing-target-service-error? (service missing-target-service-error-service) (target-type missing-target-service-error-target-type)) (define-condition-type &ambiguous-target-service-error &service-error ambiguous-target-service-error? (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." (define (add-edges service edges) (define (add-edge extension edges) (let ((target-type (service-extension-target extension))) (match (filter (lambda (service) (eq? (service-kind service) target-type)) services) ((target) (vhash-consq target service edges)) (() (missing-target-error service target-type)) (x (raise (condition (&ambiguous-target-service-error (service service) (target-type target-type)) (&message (message (format #f (G_ "more than one target service of type '~a'") (service-type-name target-type)))))))))) (fold add-edge edges (service-type-extensions (service-kind service)))) (let ((edges (fold add-edges vlist-null services))) (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 TARGET-TYPE; return the root service adjusted accordingly." (define dependents (service-back-edges services)) (define (matching-extension target) (let ((target (service-kind target))) (match-lambda (($ <service-extension> type) (eq? type target))))) (define (apply-extension target) (lambda (service) (match (find (matching-extension target) (service-type-extensions (service-kind service))) (($ <service-extension> _ compute) (compute (service-value service)))))) (match (filter (lambda (service) (eq? (service-kind service) target-type)) services) ((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 (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 (service #f) (target-type target-type)) (&message (message (format #f (G_ "more than one target service of type '~a'") (service-type-name target-type))))))))) (define (remove-service-extensions type lst) "Return TYPE, a service type, without any of the service extensions targeting one of the types in LST." (service-type (inherit type) (extensions (remove (lambda (extension) (memq (service-extension-target extension) lst)) (service-type-extensions type))))) (define-syntax-parameter for-home? ;; Whether the configuration being defined is for a Home service. (identifier-syntax #f)) (define-syntax-rule (for-home exp ...) "Mark EXP, which typically defines a service configuration, as targeting a Home service rather than a System service." (syntax-parameterize ((for-home? (identifier-syntax #t))) exp ...)) (define-with-syntax-properties (validate-service-list (value properties)) (%validate-service-list value properties)) (define (%validate-service-list value properties) (match value (((? service?) ...) value) (_ (raise (make-compound-condition (condition (&error-location (location (source-properties->location properties)))) (formatted-message (G_ "'services' field must contain a list of services"))))))) ;;; services.scm ends here.