X-Git-Url: https://git.hcoop.net/jackhill/guix/guix.git/blobdiff_plain/69daee23af49aeafcb1d250c90860f9253da719e..24297a9a9cc36de88ed2a6d30ba2ed9acc34407b:/gnu/services.scm diff --git a/gnu/services.scm b/gnu/services.scm index 5c314748da..27e5558231 100644 --- a/gnu/services.scm +++ b/gnu/services.scm @@ -1,6 +1,7 @@ ;;; GNU Guix --- Functional package management for GNU -;;; Copyright © 2015, 2016, 2017 Ludovic Courtès +;;; Copyright © 2015, 2016, 2017, 2018, 2019, 2020 Ludovic Courtès ;;; Copyright © 2016 Chris Marusich +;;; Copyright © 2020 Jan (janneke) Nieuwenhuizen ;;; ;;; This file is part of GNU Guix. ;;; @@ -23,12 +24,17 @@ #: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 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 +43,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 @@ -49,6 +56,12 @@ 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 +72,7 @@ simple-service modify-services service-back-edges + instantiate-missing-services fold-services service-error? @@ -73,11 +87,14 @@ ambiguous-target-service-error-target-type system-service-type + provenance-service-type 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 @@ -89,9 +106,7 @@ %boot-service %activation-service - etc-service - - file-union)) ;XXX: for lack of a better place + etc-service)) ;;; Comment: ;;; @@ -145,7 +160,15 @@ ;; 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 ; + (default (and=> (current-source-location) + source-properties->location)) + (innate))) (define (write-service-type type port) (format port "#" @@ -154,6 +177,43 @@ (set-record-type-printer! 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 (make-service type value) @@ -262,11 +322,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 +339,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 +361,157 @@ 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)) + +;;; +;;; 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." + `(channel (name ',(channel-name channel)) + (url ,(channel-url channel)) + (branch ,(channel-branch channel)) + (commit ,(channel-commit channel)))) + +(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." + `(channel (name ,(channel-name channel)) + (url ,(channel-url channel)) + (branch ,(channel-branch channel)) + (commit ,(channel-commit channel)))) + +(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 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."))) + + +;;; +;;; 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 + ;; . + (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 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 +520,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 +548,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 +561,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 +583,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 +607,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 +650,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 +668,23 @@ 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 (condition + (&message + (message (format #f (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 +704,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 +720,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 +740,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 +760,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 +791,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 '()))) ;;; @@ -606,6 +812,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 , returns the list of objects that depend on it." @@ -618,15 +836,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 +854,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,18 +920,34 @@ 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