aboutsummaryrefslogtreecommitdiff
path: root/tests/monads.scm
blob: 18bf4119bed816dc85f1cbe5fc0252239cfd43ef (about) (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
;;; GNU Guix --- Functional package management for GNU
;;; Copyright © 2013, 2014, 2015, 2016 Ludovic Courtès <ludo@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 (test-monads)
  #:use-module (guix tests)
  #:use-module (guix store)
  #:use-module (guix monads)
  #:use-module (guix grafts)
  #:use-module (guix derivations)
  #:use-module (guix packages)
  #:use-module (gnu packages)
  #:use-module (gnu packages bootstrap)
  #:use-module ((gnu packages base) #:select (coreutils))
  #:use-module (ice-9 match)
  #:use-module (rnrs io ports)
  #:use-module (srfi srfi-1)
  #:use-module (srfi srfi-26)
  #:use-module (srfi srfi-64))

;; Test the (guix monads) module.

(define %store
  (open-connection-for-tests))

;; Globally disable grafts because they can trigger early builds.
(%graft? #f)

(define %monads
  (list %identity-monad %store-monad %state-monad))

(define %monad-run
  (list identity
        (cut run-with-store %store <>)
        (cut run-with-state <> '())))

(define-syntax-rule (values->list exp)
  (call-with-values (lambda () exp)
    list))


(test-begin "monads")

(test-assert "monad?"
  (and (every monad? %monads)
       (every (compose procedure? monad-bind) %monads)
       (every (compose procedure? monad-return) %monads)))

;; The 3 "monad laws": <http://www.haskell.org/haskellwiki/Monad_laws>.

(test-assert "left identity"
  (every (lambda (monad run)
           (let ((number (random 777)))
             (with-monad monad
               (define (f x)
                 (return (* (1+ number) 2)))

               (= (run (>>= (return number) f))
                  (run (f number))))))
         %monads
         %monad-run))

(test-assert "right identity"
  (every (lambda (monad run)
           (with-monad monad
             (let ((number (return (random 777))))
               (= (run (>>= number return))
                  (run number)))))
         %monads
         %monad-run))

(test-assert "associativity"
  (every (lambda (monad run)
           (with-monad monad
             (define (f x)
               (return (+ 1 x)))
             (define (g x)
               (return (* 2 x)))

             (let ((number (return (random 777))))
               (= (run (>>= (>>= number f) g))
                  (run (>>= number (lambda (x) (>>= (f x) g))))))))
         %monads
         %monad-run))

(test-assert "lift"
  (every (lambda (monad run)
           (let ((f (lift1 1+ monad))
                 (g (apply lift1 1+ (list monad))))
             (with-monad monad
               (let ((number (random 777)))
                 (= (run (>>= (return number) f))
                    (run (>>= (return number) g))
                    (1+ number))))))
         %monads
         %monad-run))

(test-assert ">>= with more than two arguments"
  (every (lambda (monad run)
           (let ((1+ (lift1 1+ monad))
                 (2* (lift1 (cut * 2 <>) monad)))
             (with-monad monad
               (let ((number (random 777)))
                 (= (run (>>= (return number)
                              1+ 1+ 1+
                              2* 2* 2*))
                    (* 8 (+ number 3)))))))
         %monads
         %monad-run))

(test-assert "mbegin"
  (every (lambda (monad run)
           (with-monad monad
             (let* ((been-there? #f)
                    (number (mbegin monad
                              (return 1)
                              (begin
                                (set! been-there? #t)
                                (return 2))
                              (return 3))))
               (and (= (run number) 3)
                    been-there?))))
         %monads
         %monad-run))

(test-assert "mlet* + text-file + package-file"
  (run-with-store %store
    (mlet* %store-monad ((guile (package-file %bootstrap-guile "bin/guile"))
                         (file  (text-file "monadic" guile)))
      (return (equal? (call-with-input-file file get-string-all)
                      guile)))
    #:guile-for-build (package-derivation %store %bootstrap-guile)))

(test-assert "package-file, default system"
  ;; The default system should be the one at '>>=' time, not the one at
  ;; invocation time.  See <http://bugs.gnu.org/18002>.
  (run-with-store %store
    (mlet* %store-monad
        ((system -> (%current-system))
         (file   (parameterize ((%current-system "foobar64-linux"))
                   (package-file coreutils "bin/ls")))
         (cu     (package->derivation coreutils)))
      (return (string=? file
                        (string-append (derivation->output-path cu)
                                       "/bin/ls"))))
    #:guile-for-build (package-derivation %store %bootstrap-guile)))

(test-assert "package-file + package->cross-derivation"
  (run-with-store %store
    (mlet* %store-monad ((target -> "mips64el-linux-gnu")
                         (file (package-file coreutils "bin/ls"
                                             #:target target))
                         (xcu  (package->cross-derivation coreutils target)))
      (let ((output (derivation->output-path xcu)))
        (return (string=? file (string-append output "/bin/ls")))))
    #:guile-for-build (package-derivation %store %bootstrap-guile)))

(test-assert "interned-file"
  (run-with-store %store
    (mlet* %store-monad ((file -> (search-path %load-path "guix.scm"))
                         (a       (interned-file file))
                         (b       (interned-file file "b")))
      (return (equal? (call-with-input-file file get-string-all)
                      (call-with-input-file a get-string-all)
                      (call-with-input-file b get-string-all))))
    #:guile-for-build (package-derivation %store %bootstrap-guile)))

(test-assert "mapm"
  (every (lambda (monad run)
           (with-monad monad
             (equal? (run (mapm monad (lift1 1+ monad) (iota 10)))
                     (map 1+ (iota 10)))))
         %monads
         %monad-run))

(test-assert "sequence"
  (every (lambda (monad run)
           (let* ((input (iota 100))
                  (order '()))
             (define (frob i)
               (mlet monad ((foo (return 'foo)))
                 ;; The side effect here is used to keep track of the order in
                 ;; which monadic values are bound.  Perform the side effect
                 ;; within a '>>=' so that it is performed when the return
                 ;; value is actually bound.
                 (set! order (cons i order))
                 (return i)))

             (and (equal? input
                          (run (sequence monad (map frob input))))

                  ;; Make sure this is from left to right.
                  (equal? order (reverse input)))))
         %monads
         %monad-run))

(test-assert "listm"
  (every (lambda (monad run)
           (run (with-monad monad
                  (let ((lst (listm monad
                                    (return 1) (return 2) (return 3))))
                    (mlet monad ((lst lst))
                      (return (equal? '(1 2 3) lst)))))))
         %monads
         %monad-run))

(test-assert "anym"
  (every (lambda (monad run)
           (eq? (run (with-monad monad
                       (anym monad
                             (lift1 (lambda (x)
                                      (and (odd? x) 'odd!))
                                    monad)
                             (append (make-list 1000 0)
                                     (list 1 2)))))
                'odd!))
         %monads
         %monad-run))

(test-equal "set-current-state"
  (list '(a a d) 'd)
  (values->list
   (run-with-state
       (mlet* %state-monad ((init  (current-state))
                            (init2 (set-current-state 'b)))
         (mbegin %state-monad
           (set-current-state 'c)
           (set-current-state 'd)
           (mlet %state-monad ((last (current-state)))
             (return (list init init2 last)))))
     'a)))

(test-equal "state-push etc."
  (list '((z . 2) (p . (1)) (a . (1))) '(2 1))
  (values->list
   (run-with-state
       (mbegin %state-monad
         (state-push 1)    ;(1)
         (state-push 2)    ;(2 1)
         (mlet* %state-monad ((z (state-pop))        ;(1)
                              (p (current-state))
                              (a (state-push z)))    ;(2 1)
           (return `((z . ,z) (p . ,p) (a . ,a)))))
     '())))

(test-end "monads")
(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 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 (libc-utf8-locales-for-target (%current-system)) "/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))) (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 managed 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))) #~(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.