guix-devel/guix/scripts/system.scm

758 lines
28 KiB
Scheme
Raw Normal View History

;;; GNU Guix --- Functional package management for GNU
monads: Move '%store-monad' and related procedures where they belong. This turns (guix monads) into a generic module for monads, and moves the store monad and related monadic procedures in their corresponding module. * guix/monads.scm (store-return, store-bind, %store-monad, store-lift, text-file, interned-file, package-file, package->derivation, package->cross-derivation, origin->derivation, imported-modules, compiled, modules, built-derivations, run-with-store): Move to... * guix/store.scm (store-return, store-bind, %store-monad, store-lift, text-file, interned-file): ... here. (%guile-for-build): New variable. (run-with-store): Moved from monads.scm. Remove default value for #:guile-for-build. * guix/packages.scm (default-guile): Export. (set-guile-for-build): New procedure. (package-file, package->derivation, package->cross-derivation, origin->derivation): Moved from monads.scm. * guix/derivations.scm (%guile-for-build): Remove. (imported-modules): Rename to... (%imported-modules): ... this. (compiled-modules): Rename to... (%compiled-modules): ... this. (built-derivations, imported-modules, compiled-modules): New procedures. * gnu/services/avahi.scm, gnu/services/base.scm, gnu/services/dbus.scm, gnu/services/dmd.scm, gnu/services/networking.scm, gnu/services/ssh.scm, gnu/services/xorg.scm, gnu/system/install.scm, gnu/system/linux-initrd.scm, gnu/system/shadow.scm, guix/download.scm, guix/gexp.scm, guix/git-download.scm, guix/profiles.scm, guix/svn-download.scm, tests/monads.scm: Adjust imports accordingly. * guix/monad-repl.scm (default-guile-derivation): New procedure. (store-monad-language, run-in-store): Use it. * build-aux/hydra/gnu-system.scm (qemu-jobs): Add explicit 'set-guile-for-build' call. * guix/scripts/archive.scm (derivation-from-expression): Likewise. * guix/scripts/build.scm (options/resolve-packages): Likewise. * guix/scripts/environment.scm (guix-environment): Likewise. * guix/scripts/system.scm (guix-system): Likewise. * doc/guix.texi (The Store Monad): Adjust module names accordingly.
2015-01-14 13:34:52 +01:00
;;; Copyright © 2014, 2015 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 (guix scripts system)
#:use-module (guix config)
#:use-module (guix ui)
#:use-module (guix store)
#:use-module (guix gexp)
#:use-module (guix derivations)
#:use-module (guix packages)
#:use-module (guix utils)
#:use-module (guix monads)
#:use-module (guix records)
#:use-module (guix profiles)
#:use-module (guix scripts)
#:use-module (guix scripts build)
#:use-module (guix graph)
#:use-module (guix scripts graph)
#:use-module (guix build utils)
#:use-module (gnu build install)
#:use-module (gnu system)
#:use-module (gnu system file-systems)
#:use-module (gnu system linux-container)
#:use-module (gnu system vm)
#:use-module (gnu system grub)
#:use-module (gnu services)
#:use-module (gnu services dmd)
#:use-module (gnu packages grub)
#:use-module (srfi srfi-1)
#:use-module (srfi srfi-19)
#:use-module (srfi srfi-26)
#:use-module (srfi srfi-34)
#:use-module (srfi srfi-35)
#:use-module (srfi srfi-37)
#:use-module (ice-9 match)
#:export (guix-system
read-operating-system))
;;;
;;; Operating system declaration.
;;;
(define %user-module
;; Module in which the machine description file is loaded.
(make-user-module '((gnu system)
(gnu services)
(gnu system shadow))))
(define (read-operating-system file)
"Read the operating-system declaration from FILE and return it."
(load* file %user-module))
;;;
;;; Installation.
;;;
;; TODO: Factorize.
(define references*
(store-lift references))
(define topologically-sorted*
(store-lift topologically-sorted))
(define* (copy-item item target
#:key (log-port (current-error-port)))
"Copy ITEM to the store under root directory TARGET and register it."
(mlet* %store-monad ((refs (references* item)))
(let ((dest (string-append target item))
(state (string-append target "/var/guix")))
(format log-port "copying '~a'...~%" item)
;; Remove DEST if it exists to make sure that (1) we do not fail badly
;; while trying to overwrite it (see <http://bugs.gnu.org/20722>), and
;; (2) we end up with the right contents.
(when (file-exists? dest)
(delete-file-recursively dest))
(copy-recursively item dest
#:log (%make-void-port "w"))
;; Register ITEM; as a side-effect, it resets timestamps, etc.
;; Explicitly use "TARGET/var/guix" as the state directory, to avoid
;; reproducing the user's current settings; see
;; <http://bugs.gnu.org/18049>.
(unless (register-path item
#:prefix target
#:state-directory state
#:references refs)
(leave (_ "failed to register '~a' under '~a'~%")
item target))
(return #t))))
(define* (copy-closure item target
#:key (log-port (current-error-port)))
"Copy ITEM and all its dependencies to the store under root directory
TARGET, and register them."
(mlet* %store-monad ((refs (references* item))
(to-copy (topologically-sorted*
(delete-duplicates (cons item refs)
string=?))))
(sequence %store-monad
(map (cut copy-item <> target #:log-port log-port)
to-copy))))
(define (install-grub* grub.cfg device target)
"This is a variant of 'install-grub' with error handling, lifted in
%STORE-MONAD"
(let* ((gc-root (string-append %gc-roots-directory "/grub.cfg"))
(temp-gc-root (string-append gc-root ".new"))
(delete-file (lift1 delete-file %store-monad))
(make-symlink (lift2 switch-symlinks %store-monad))
(rename (lift2 rename-file %store-monad)))
(mbegin %store-monad
;; Prepare the symlink to GRUB.CFG to make sure that it's a GC root when
;; 'install-grub' completes (being a bit paranoid.)
(make-symlink temp-gc-root grub.cfg)
(munless (false-if-exception (install-grub grub.cfg device target))
(delete-file temp-gc-root)
(leave (_ "failed to install GRUB on device '~a'~%") device))
;; Register GRUB.CFG as a GC root so that its dependencies (background
;; image, font, etc.) are not reclaimed.
(rename temp-gc-root gc-root))))
(define* (install os-drv target
#:key (log-port (current-output-port))
grub? grub.cfg device)
"Copy the closure of GRUB.CFG, which includes the output of OS-DRV, to
directory TARGET. TARGET must be an absolute directory name since that's what
'guix-register' expects.
When GRUB? is true, install GRUB on DEVICE, using GRUB.CFG."
(define (maybe-copy to-copy)
(with-monad %store-monad
(if (string=? target "/")
(begin
(warning (_ "initializing the current root file system~%"))
(return #t))
(begin
;; Make sure the target store exists.
(mkdir-p (string-append target (%store-prefix)))
;; Copy items to the new store.
(copy-closure to-copy target #:log-port log-port)))))
;; Make sure TARGET is root-owned when running as root, but still allow
;; non-root uses (useful for testing.) See
;; <http://lists.gnu.org/archive/html/guix-devel/2015-05/msg00452.html>.
(if (zero? (geteuid))
(chown target 0 0)
(warning (_ "not running as 'root', so \
the ownership of '~a' may be incorrect!~%")
target))
(chmod target #o755)
(let ((os-dir (derivation->output-path os-drv))
(format (lift format %store-monad))
(populate (lift2 populate-root-file-system %store-monad)))
(mbegin %store-monad
;; Copy the closure of GRUB.CFG, which includes OS-DIR, GRUB's
;; background image and so on.
(maybe-copy grub.cfg)
;; Create a bunch of additional files.
(format log-port "populating '~a'...~%" target)
(populate os-dir target)
(mwhen grub?
(install-grub* grub.cfg device target)))))
;;;
;;; Boot parameters
;;;
(define-record-type* <boot-parameters>
boot-parameters make-boot-parameters boot-parameters?
(label boot-parameters-label)
(root-device boot-parameters-root-device)
(kernel boot-parameters-kernel)
(kernel-arguments boot-parameters-kernel-arguments))
(define (read-boot-parameters port)
"Read boot parameters from PORT and return the corresponding
<boot-parameters> object or #f if the format is unrecognized."
(match (read port)
(('boot-parameters ('version 0)
('label label) ('root-device root)
('kernel linux)
rest ...)
(boot-parameters
(label label)
(root-device root)
(kernel linux)
(kernel-arguments
(match (assq 'kernel-arguments rest)
((_ args) args)
(#f '()))))) ;the old format
(x ;unsupported format
(warning (_ "unrecognized boot parameters for '~a'~%")
system)
#f)))
;;;
;;; Reconfiguration.
;;;
(define %system-profile
;; The system profile.
(string-append %state-directory "/profiles/system"))
(define-syntax-rule (save-environment-excursion body ...)
"Save the current environment variables, run BODY..., and restore them."
(let ((env (environ)))
(dynamic-wind
(const #t)
(lambda ()
body ...)
(lambda ()
(environ env)))))
(define* (switch-to-system os
#:optional (profile %system-profile))
"Make a new generation of PROFILE pointing to the directory of OS, switch to
it atomically, and then run OS's activation script."
(mlet* %store-monad ((drv (operating-system-derivation os))
(script (operating-system-activation-script os)))
(let* ((system (derivation->output-path drv))
(number (+ 1 (generation-number profile)))
(generation (generation-file-name profile number)))
(symlink system generation)
(switch-symlinks profile generation)
(format #t (_ "activating system...~%"))
;; The activation script may change $PATH, among others, so protect
;; against that.
(return (save-environment-excursion
;; Tell 'activate-current-system' what the new system is.
(setenv "GUIX_NEW_SYSTEM" system)
(primitive-load (derivation->output-path script))))
;; TODO: Run 'deco reload ...'.
)))
(define-syntax-rule (unless-file-not-found exp)
(catch 'system-error
(lambda ()
exp)
(lambda args
(if (= ENOENT (system-error-errno args))
#f
(apply throw args)))))
(define (seconds->string seconds)
"Return a string representing the date for SECONDS."
(let ((time (make-time time-utc 0 seconds)))
(date->string (time-utc->date time)
"~Y-~m-~d ~H:~M")))
(define* (previous-grub-entries #:optional (profile %system-profile))
"Return a list of 'menu-entry' for the generations of PROFILE."
(define (system->grub-entry system number time)
(unless-file-not-found
(let ((file (string-append system "/parameters")))
(match (call-with-input-file file read-boot-parameters)
(($ <boot-parameters> label root kernel kernel-arguments)
(menu-entry
(label (string-append label " (#"
(number->string number) ", "
(seconds->string time) ")"))
(linux kernel)
(linux-arguments
(cons* (string-append "--root=" root)
#~(string-append "--system=" #$system)
#~(string-append "--load=" #$system "/boot")
kernel-arguments))
(initrd #~(string-append #$system "/initrd"))))
(#f ;invalid format
#f)))))
(let* ((numbers (generation-numbers profile))
(systems (map (cut generation-file-name profile <>)
numbers))
(times (map (lambda (system)
(unless-file-not-found
(stat:mtime (lstat system))))
systems)))
(filter-map system->grub-entry systems numbers times)))
;;;
;;; Graphs.
;;;
(define (service-node-label service)
"Return a label to represent SERVICE."
(let ((type (service-kind service))
(value (service-parameters service)))
(string-append (symbol->string (service-type-name type))
(cond ((or (number? value) (symbol? value))
(string-append " " (object->string value)))
((string? value)
(string-append " " value))
((file-system? value)
(string-append " " (file-system-mount-point value)))
(else
"")))))
(define (service-node-type services)
"Return a node type for SERVICES. Since <service> instances are not
self-contained (they express dependencies on service types, not on services),
we have to create the 'edges' procedure dynamically as a function of the full
list of services."
(node-type
(name "service")
(description "the DAG of services")
(identifier (lift1 object-address %store-monad))
(label service-node-label)
(edges (lift1 (service-back-edges services) %store-monad))))
(define (dmd-service-node-label service)
"Return a label for a node representing a <dmd-service>."
(string-join (map symbol->string (dmd-service-provision service))))
(define (dmd-service-node-type services)
"Return a node type for SERVICES, a list of <dmd-service>."
(node-type
(name "dmd-service")
(description "the dependency graph of dmd services")
(identifier (lift1 dmd-service-node-label %store-monad))
(label dmd-service-node-label)
(edges (lift1 (dmd-service-back-edges services) %store-monad))))
;;;
;;; Generations.
;;;
(define* (display-system-generation number
#:optional (profile %system-profile))
"Display a summary of system generation NUMBER in a human-readable format."
(unless (zero? number)
(let* ((generation (generation-file-name profile number))
(param-file (string-append generation "/parameters"))
(params (call-with-input-file param-file read-boot-parameters)))
(display-generation profile number)
(format #t (_ " file name: ~a~%") generation)
(format #t (_ " canonical file name: ~a~%") (readlink* generation))
(match params
(($ <boot-parameters> label root kernel)
;; TRANSLATORS: Please preserve the two-space indentation.
(format #t (_ " label: ~a~%") label)
(format #t (_ " root device: ~a~%") root)
(format #t (_ " kernel: ~a~%") kernel))
(_
#f)))))
(define* (list-generations pattern #:optional (profile %system-profile))
"Display in a human-readable format all the system generations matching
PATTERN, a string. When PATTERN is #f, display all the system generations."
(cond ((not (file-exists? profile)) ; XXX: race condition
(raise (condition (&profile-not-found-error
(profile profile)))))
((string-null? pattern)
(for-each display-system-generation (profile-generations profile)))
((matching-generations pattern profile)
=>
(lambda (numbers)
(if (null-list? numbers)
(exit 1)
(leave-on-EPIPE
(for-each display-system-generation numbers)))))
(else
(leave (_ "invalid syntax: ~a~%") pattern))))
;;;
;;; Action.
;;;
(define* (system-derivation-for-action os action
#:key image-size full-boot? mappings)
"Return as a monadic value the derivation for OS according to ACTION."
(case action
((build init reconfigure)
(operating-system-derivation os))
((container)
(container-script os #:mappings mappings))
((vm-image)
(system-qemu-image os #:disk-image-size image-size))
((vm)
(system-qemu-image/shared-store-script os
#:full-boot? full-boot?
#:disk-image-size image-size
#:mappings mappings))
((disk-image)
(system-disk-image os #:disk-image-size image-size))))
(define* (perform-action action os
#:key grub? dry-run? derivations-only?
use-substitutes? device target
image-size full-boot?
(mappings '()))
"Perform ACTION for OS. GRUB? specifies whether to install GRUB; DEVICE is
the target devices for GRUB; TARGET is the target root directory; IMAGE-SIZE
is the size of the image to be built, for the 'vm-image' and 'disk-image'
actions. FULL-BOOT? is used for the 'vm' action; it determines whether to
boot directly to the kernel or to the bootloader.
When DERIVATIONS-ONLY? is true, print the derivation file name(s) without
building anything."
(define println
(cut format #t "~a~%" <>))
(mlet* %store-monad
((sys (system-derivation-for-action os action
#:image-size image-size
#:full-boot? full-boot?
#:mappings mappings))
(grub (package->derivation grub))
(grub.cfg (if (eq? 'container action)
(return #f)
(operating-system-grub.cfg os
(if (eq? 'init action)
'()
(previous-grub-entries)))))
;; For 'init' and 'reconfigure', always build GRUB.CFG, even if
;; --no-grub is passed, because GRUB.CFG because we then use it as a GC
;; root. See <http://bugs.gnu.org/21068>.
(drvs -> (if (memq action '(init reconfigure))
(if grub?
(list sys grub.cfg grub)
(list sys grub.cfg))
(list sys)))
(% (if derivations-only?
(return (for-each (compose println derivation-file-name)
drvs))
(maybe-build drvs #:dry-run? dry-run?
#:use-substitutes? use-substitutes?))))
(if (or dry-run? derivations-only?)
(return #f)
(begin
(for-each (compose println derivation->output-path)
drvs)
;; Make sure GRUB is accessible.
(when grub?
(let ((prefix (derivation->output-path grub)))
(setenv "PATH"
(string-append prefix "/bin:" prefix "/sbin:"
(getenv "PATH")))))
(case action
((reconfigure)
(mbegin %store-monad
(switch-to-system os)
(mwhen grub?
(install-grub* (derivation->output-path grub.cfg)
device "/"))))
((init)
(newline)
(format #t (_ "initializing operating system under '~a'...~%")
target)
(install sys (canonicalize-path target)
#:grub? grub?
#:grub.cfg (derivation->output-path grub.cfg)
#:device device))
(else
;; All we had to do was to build SYS.
(return (derivation->output-path sys))))))))
(define (export-extension-graph os port)
"Export the service extension graph of OS to PORT."
(let* ((services (operating-system-services os))
(system (find (lambda (service)
(eq? (service-kind service) system-service-type))
services)))
(export-graph (list system) (current-output-port)
#:node-type (service-node-type services)
#:reverse-edges? #t)))
(define (export-dmd-graph os port)
"Export the graph of dmd services of OS to PORT."
(let* ((services (operating-system-services os))
(pid1 (fold-services services
#:target-type dmd-root-service-type))
(dmds (service-parameters pid1)) ;the list of <dmd-service>
(sinks (filter (lambda (service)
(null? (dmd-service-requirement service)))
dmds)))
(export-graph sinks (current-output-port)
#:node-type (dmd-service-node-type dmds)
#:reverse-edges? #t)))
;;;
;;; Options.
;;;
(define (show-help)
(display (_ "Usage: guix system [OPTION] ACTION [FILE]
Build the operating system declared in FILE according to ACTION.\n"))
(newline)
(display (_ "The valid values for ACTION are:\n"))
(newline)
(display (_ "\
reconfigure switch to a new operating system configuration\n"))
(display (_ "\
list-generations list the system generations\n"))
(display (_ "\
build build the operating system without installing anything\n"))
(display (_ "\
container build a container that shares the host's store\n"))
(display (_ "\
vm build a virtual machine image that shares the host's store\n"))
(display (_ "\
vm-image build a freestanding virtual machine image\n"))
(display (_ "\
disk-image build a disk image, suitable for a USB stick\n"))
(display (_ "\
init initialize a root file system to run GNU\n"))
(display (_ "\
extension-graph emit the service extension graph in Dot format\n"))
(display (_ "\
dmd-graph emit the graph of dmd services in Dot format\n"))
(show-build-options-help)
(display (_ "
-d, --derivation return the derivation of the given system"))
(display (_ "
--on-error=STRATEGY
apply STRATEGY when an error occurs while reading FILE"))
(display (_ "
--image-size=SIZE for 'vm-image', produce an image of SIZE"))
(display (_ "
--no-grub for 'init', do not install GRUB"))
(display (_ "
--share=SPEC for 'vm', share host file system according to SPEC"))
(display (_ "
--expose=SPEC for 'vm', expose host file system according to SPEC"))
(display (_ "
--full-boot for 'vm', make a full boot sequence"))
(newline)
(display (_ "
-h, --help display this help and exit"))
(display (_ "
-V, --version display version information and exit"))
(newline)
(show-bug-report-information))
(define %options
;; Specifications of the command-line options.
(cons* (option '(#\h "help") #f #f
(lambda args
(show-help)
(exit 0)))
(option '(#\V "version") #f #f
(lambda args
(show-version-and-exit "guix system")))
(option '(#\d "derivation") #f #f
(lambda (opt name arg result)
(alist-cons 'derivations-only? #t result)))
(option '("on-error") #t #f
(lambda (opt name arg result)
(alist-cons 'on-error (string->symbol arg)
result)))
(option '("image-size") #t #f
(lambda (opt name arg result)
(alist-cons 'image-size (size->number arg)
result)))
(option '("no-grub") #f #f
(lambda (opt name arg result)
(alist-cons 'install-grub? #f result)))
(option '("full-boot") #f #f
(lambda (opt name arg result)
(alist-cons 'full-boot? #t result)))
(option '("share") #t #f
(lambda (opt name arg result)
(alist-cons 'file-system-mapping
(specification->file-system-mapping arg #t)
result)))
(option '("expose") #t #f
(lambda (opt name arg result)
(alist-cons 'file-system-mapping
(specification->file-system-mapping arg #f)
result)))
(option '(#\n "dry-run") #f #f
(lambda (opt name arg result)
(alist-cons 'dry-run? #t result)))
(option '(#\s "system") #t #f
(lambda (opt name arg result)
(alist-cons 'system arg
(alist-delete 'system result eq?))))
%standard-build-options))
(define %default-options
;; Alist of default option values.
`((system . ,(%current-system))
(substitutes? . #t)
(build-hook? . #t)
(max-silent-time . 3600)
(verbosity . 0)
(image-size . ,(* 900 (expt 2 20)))
(install-grub? . #t)))
;;;
;;; Entry point.
;;;
(define (process-action action args opts)
"Process ACTION, a sub-command, with the arguments are listed in ARGS.
ACTION must be one of the sub-commands that takes an operating system
declaration as an argument (a file name.) OPTS is the raw alist of options
resulting from command-line parsing."
(let* ((file (match args
(() #f)
((x . _) x)))
(system (assoc-ref opts 'system))
(os (if file
(load* file %user-module
#:on-error (assoc-ref opts 'on-error))
(leave (_ "no configuration file specified~%"))))
(dry? (assoc-ref opts 'dry-run?))
(grub? (assoc-ref opts 'install-grub?))
(target (match args
((first second) second)
(_ #f)))
(device (and grub?
(grub-configuration-device
(operating-system-bootloader os)))))
(with-store store
(set-build-options-from-command-line store opts)
(run-with-store store
(mbegin %store-monad
(set-guile-for-build (default-guile))
(case action
((extension-graph)
(export-extension-graph os (current-output-port)))
((dmd-graph)
(export-dmd-graph os (current-output-port)))
(else
(perform-action action os
#:dry-run? dry?
#:derivations-only? (assoc-ref opts
'derivations-only?)
#:use-substitutes? (assoc-ref opts 'substitutes?)
#:image-size (assoc-ref opts 'image-size)
#:full-boot? (assoc-ref opts 'full-boot?)
#:mappings (filter-map (match-lambda
(('file-system-mapping . m)
m)
(_ #f))
opts)
#:grub? grub?
#:target target #:device device))))
#:system system))))
(define (process-command command args opts)
"Process COMMAND, one of the 'guix system' sub-commands. ARGS is its
argument list and OPTS is the option alist."
(case command
((list-generations)
;; List generations. No need to connect to the daemon, etc.
(let ((pattern (match args
(() "")
((pattern) pattern)
(x (leave (_ "wrong number of arguments~%"))))))
(list-generations pattern)))
(else
(process-action command args opts))))
(define (guix-system . args)
(define (parse-sub-command arg result)
;; Parse sub-command ARG and augment RESULT accordingly.
(if (assoc-ref result 'action)
(alist-cons 'argument arg result)
(let ((action (string->symbol arg)))
(case action
((build container vm vm-image disk-image reconfigure init
extension-graph dmd-graph list-generations)
(alist-cons 'action action result))
(else (leave (_ "~a: unknown action~%") action))))))
(define (match-pair car)
;; Return a procedure that matches a pair with CAR.
(match-lambda
((head . tail)
(and (eq? car head) tail))
(_ #f)))
(define (option-arguments opts)
;; Extract the plain arguments from OPTS.
(let* ((args (reverse (filter-map (match-pair 'argument) opts)))
(count (length args))
(action (assoc-ref opts 'action)))
(define (fail)
(leave (_ "wrong number of arguments for action '~a'~%")
action))
(unless action
(format (current-error-port)
(_ "guix system: missing command name~%"))
(format (current-error-port)
(_ "Try 'guix system --help' for more information.~%"))
(exit 1))
(case action
((build container vm vm-image disk-image reconfigure)
(unless (= count 1)
(fail)))
((init)
(unless (= count 2)
(fail))))
args))
(with-error-handling
(let* ((opts (parse-command-line args %options
(list %default-options)
#:argument-handler
parse-sub-command))
(args (option-arguments opts))
(command (assoc-ref opts 'action)))
(process-command command args opts))))
;;; system.scm ends here