services: Add 'system-service-type'.
[jackhill/guix/guix.git] / guix / scripts / system.scm
index 823713e..7a8a751 100644 (file)
@@ -1,5 +1,5 @@
 ;;; GNU Guix --- Functional package management for GNU
-;;; Copyright © 2014 Ludovic Courtès <ludo@gnu.org>
+;;; Copyright © 2014, 2015 Ludovic Courtès <ludo@gnu.org>
 ;;;
 ;;; This file is part of GNU Guix.
 ;;;
 ;;; 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 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))
 
+\f
+;;;
+;;; Operating system declaration.
+;;;
+
 (define %user-module
   ;; Module in which the machine description file is loaded.
-  (let ((module (make-fresh-user-module)))
-    (for-each (lambda (iface)
-                (module-use! module (resolve-interface iface)))
-              '((gnu system)
-                (gnu services)
-                (gnu system shadow)))
-    module))
+  (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."
-  ;; TODO: Factorize.
-  (catch #t
-    (lambda ()
-      ;; Avoid ABI incompatibility with the <operating-system> record.
-      (set! %fresh-auto-compile #t)
+  (load* file %user-module))
+
+
+\f
+;;;
+;;; 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)
 
-      (save-module-excursion
-       (lambda ()
-         (set-current-module %user-module)
-         (primitive-load file))))
+      (mwhen grub?
+        (install-grub* grub.cfg device target)))))
+
+\f
+;;;
+;;; 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)))
+
+\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
-      (match args
-        (('system-error . _)
-         (let ((err (system-error-errno args)))
-           (leave (_ "failed to open operating system file '~a': ~a~%")
-                  file (strerror err))))
+      (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)))
+
+\f
+;;;
+;;; 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))))
+
+\f
+;;;
+;;; 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))
         (_
-         (leave (_ "failed to load machine file '~a': ~s~%")
-                file args))))))
+         #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))))
+
+\f
+;;;
+;;; 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)))))
+       (drvs   -> (if (and grub? (memq action '(init reconfigure)))
+                      (list sys grub 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)))
 
 \f
 ;;;
 ;;;
 
 (define (show-help)
-  (display (_ "Usage: guix system [OPTION] ACTION FILE
+  (display (_ "Usage: guix system [OPTION] ACTION [FILE]
 Build the operating system declared in FILE according to ACTION.\n"))
-  (display (_ "Currently the only valid value for ACTION is 'vm', which builds
-a virtual machine of the given operating system.\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"))
@@ -91,9 +576,42 @@ a virtual machine of the given operating system.\n"))
          (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
@@ -102,48 +620,131 @@ a virtual machine of the given operating system.\n"))
     (substitutes? . #t)
     (build-hook? . #t)
     (max-silent-time . 3600)
-    (verbosity . 0)))
+    (verbosity . 0)
+    (image-size . ,(* 900 (expt 2 20)))
+    (install-grub? . #t)))
 
 \f
 ;;;
 ;;; 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-options)
-    ;; Return the alist of option values.
-    (args-fold* args %options
-                (lambda (opt name arg result)
-                  (leave (_ "~A: unrecognized option~%") name))
-                (lambda (arg result)
-                  (if (assoc-ref result 'action)
-                      (let ((previous (assoc-ref result 'argument)))
-                        (if previous
-                            (leave (_ "~a: extraneous argument~%") previous)
-                            (alist-cons 'argument arg result)))
-                      (let ((action (string->symbol arg)))
-                        (case action
-                          ((vm) (alist-cons 'action action result))
-                          (else (leave (_ "~a: unknown action~%")
-                                       action))))))
-                %default-options))
+  (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-options))
-           (file  (assoc-ref opts 'argument))
-           (os    (if file
-                      (read-operating-system file)
-                      (leave (_ "no configuration file specified~%"))))
-           (mdrv  (system-qemu-image/shared-store-script os))
-           (store (open-connection))
-           (dry?  (assoc-ref opts 'dry-run?))
-           (drv   (run-with-store store mdrv)))
-      (set-build-options-from-command-line store opts)
-      (show-what-to-build store (list drv)
-                          #:dry-run? dry?
-                          #:use-substitutes? (assoc-ref opts 'substitutes?))
-
-      (unless dry?
-        (build-derivations store (list drv))
-        (display (derivation->output-path drv))
-        (newline)))))
+    (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