build: Add iso9660 system image generator.
[jackhill/guix/guix.git] / gnu / build / vm.scm
index 2c53cf5..860c983 100644 (file)
@@ -1,5 +1,9 @@
 ;;; GNU Guix --- Functional package management for GNU
-;;; Copyright © 2013, 2014 Ludovic Courtès <ludo@gnu.org>
+;;; Copyright © 2013, 2014, 2015, 2016, 2017 Ludovic Courtès <ludo@gnu.org>
+;;; Copyright © 2016 Christopher Allan Webber <cwebber@dustycloud.org>
+;;; Copyright © 2016 Leo Famulari <leo@famulari.name>
+;;; Copyright © 2017 Mathieu Othacehe <m.othacehe@gmail.com>
+;;; Copyright © 2017 Marius Bakke <mbakke@fastmail.com>
 ;;;
 ;;; This file is part of GNU Guix.
 ;;;
 (define-module (gnu build vm)
   #:use-module (guix build utils)
   #:use-module (guix build store-copy)
+  #:use-module (guix build syscalls)
   #:use-module (gnu build linux-boot)
   #:use-module (gnu build install)
+  #:use-module (guix records)
+  #:use-module ((guix combinators) #:select (fold2))
+  #:use-module (ice-9 format)
   #:use-module (ice-9 match)
   #:use-module (ice-9 regex)
+  #:use-module (srfi srfi-1)
+  #:use-module (srfi srfi-9)
   #:use-module (srfi srfi-26)
   #:export (qemu-command
             load-in-linux-vm
             format-partition
-            initialize-root-partition
+
+            partition
+            partition?
+            partition-device
+            partition-size
+            partition-file-system
+            partition-label
+            partition-flags
+            partition-initializer
+
+            estimated-partition-size
+            root-partition-initializer
             initialize-partition-table
-            initialize-hard-disk))
+            initialize-hard-disk
+            make-iso9660-image))
 
 ;;; Commentary:
 ;;;
@@ -40,8 +62,8 @@
 
 (define* (qemu-command #:optional (system %host-type))
   "Return the default name of the QEMU command for SYSTEM."
-  (let ((cpu (substring %host-type 0
-                        (string-index %host-type #\-))))
+  (let ((cpu (substring system 0
+                        (string-index system #\-))))
     (string-append "qemu-system-"
                    (if (string-match "^i[3456]86$" cpu)
                        "i386"
                            output
                            (qemu (qemu-command)) (memory-size 512)
                            linux initrd
-                           make-disk-image? (disk-image-size 100)
+                           make-disk-image?
+                           (disk-image-size (* 100 (expt 2 20)))
                            (disk-image-format "qcow2")
                            (references-graphs '()))
   "Run BUILDER, a Scheme file, into a VM running LINUX with INITRD, and copy
 the result to OUTPUT.
 
 When MAKE-DISK-IMAGE? is true, OUTPUT will contain a VM image of
-DISK-IMAGE-SIZE MiB resulting from the execution of BUILDER, which may access
-it via /dev/hda.
+DISK-IMAGE-SIZE bytes resulting from the execution of BUILDER, which may
+access it via /dev/hda.
 
 REFERENCES-GRAPHS can specify a list of reference-graph files as produced by
 the #:references-graphs parameter of 'derivation'."
-  (define image-file
-    (string-append "image." disk-image-format))
-
   (when make-disk-image?
+    (format #t "creating ~a image of ~,2f MiB...~%"
+            disk-image-format (/ disk-image-size (expt 2 20)))
+    (force-output)
     (unless (zero? (system* "qemu-img" "create" "-f" disk-image-format
-                            image-file
+                            output
                             (number->string disk-image-size)))
       (error "qemu-img failed")))
 
@@ -84,7 +107,7 @@ the #:references-graphs parameter of 'derivation'."
     (_ #f))
 
   (unless (zero?
-           (apply system* qemu "-enable-kvm" "-nographic" "-no-reboot"
+           (apply system* qemu "-nographic" "-no-reboot"
                   "-m" (number->string memory-size)
                   "-net" "nic,model=virtio"
                   "-virtfs"
@@ -98,43 +121,111 @@ the #:references-graphs parameter of 'derivation'."
                   "-initrd" initrd
                   "-append" (string-append "console=ttyS0 --load="
                                            builder)
-                  (if make-disk-image?
-                      `("-drive" ,(string-append "file=" image-file
-                                                 ",if=virtio"))
-                      '())))
+                  (append
+                   (if make-disk-image?
+                       `("-drive" ,(string-append "file=" output
+                                                  ",if=virtio"))
+                       '())
+                   ;; Only enable kvm if we see /dev/kvm exists.
+                   ;; This allows users without hardware virtualization to still
+                   ;; use these commands.
+                   (if (file-exists? "/dev/kvm")
+                       '("-enable-kvm")
+                       '()))))
     (error "qemu failed" qemu))
 
-  (if make-disk-image?
-      (copy-file image-file output)
-      (begin
-        (mkdir output)
-        (copy-recursively "xchg" output))))
+  ;; When MAKE-DISK-IMAGE? is true, the image is in OUTPUT already.
+  (unless make-disk-image?
+    (mkdir output)
+    (copy-recursively "xchg" output)))
+
+\f
+;;;
+;;; Partitions.
+;;;
 
-(define* (initialize-partition-table device partition-size
+(define-record-type* <partition> partition make-partition
+  partition?
+  (device      partition-device (default #f))
+  (size        partition-size)
+  (file-system partition-file-system (default "ext4"))
+  (label       partition-label (default #f))
+  (flags       partition-flags (default '()))
+  (initializer partition-initializer (default (const #t))))
+
+(define (estimated-partition-size graphs)
+  "Return the estimated size of a partition that can store the store items
+given by GRAPHS, a list of file names produced by #:references-graphs."
+  ;; Simply add a 20% overhead.
+  (round (* 1.2 (closure-size graphs))))
+
+(define* (initialize-partition-table device partitions
                                      #:key
-                                     bootable?
                                      (label-type "msdos")
                                      (offset (expt 2 20)))
-  "Create on DEVICE a partition table of type LABEL-TYPE, with a single
-partition of PARTITION-SIZE bytes starting at OFFSET bytes.  When BOOTABLE? is
-true, set the bootable flag on the partition.  Return #t on success."
-  (format #t "creating partition table with a ~a B partition...\n"
-          partition-size)
-  (unless (zero? (apply system* "parted" device "mklabel" label-type
-                        "mkpart" "primary" "ext2"
-                        (format #f "~aB" offset)
-                        (format #f "~aB" partition-size)
-                        (if bootable?
-                            '("set" "1" "boot" "on")
-                            '())))
-    (error "failed to create partition table")))
+  "Create on DEVICE a partition table of type LABEL-TYPE, containing the given
+PARTITIONS (a list of <partition> objects), starting at OFFSET bytes.  On
+success, return PARTITIONS with their 'device' field changed to reflect their
+actual /dev name based on DEVICE."
+  (define (partition-options part offset index)
+    (cons* "mkpart" "primary" "ext2"
+           (format #f "~aB" offset)
+           (format #f "~aB" (+ offset (partition-size part)))
+           (append-map (lambda (flag)
+                         (list "set" (number->string index)
+                               (symbol->string flag) "on"))
+                       (partition-flags part))))
+
+  (define (options partitions offset)
+    (let loop ((partitions partitions)
+               (offset     offset)
+               (index      1)
+               (result     '()))
+      (match partitions
+        (()
+         (concatenate (reverse result)))
+        ((head tail ...)
+         (loop tail
+               ;; Leave one sector (512B) between partitions to placate
+               ;; Parted.
+               (+ offset 512 (partition-size head))
+               (+ 1 index)
+               (cons (partition-options head offset index)
+                     result))))))
+
+  (format #t "creating partition table with ~a partitions (~a)...\n"
+          (length partitions)
+          (string-join (map (compose (cut string-append <> " MiB")
+                                     number->string
+                                     (lambda (size)
+                                       (round (/ size (expt 2. 20))))
+                                     partition-size)
+                            partitions)
+                       ", "))
+  (unless (zero? (apply system* "parted" "--script"
+                        device "mklabel" label-type
+                        (options partitions offset)))
+    (error "failed to create partition table"))
+
+  ;; Set the 'device' field of each partition.
+  (reverse
+   (fold2 (lambda (part result index)
+            (values (cons  (partition
+                            (inherit part)
+                            (device (string-append device
+                                                   (number->string index))))
+                           result)
+                    (+ 1 index)))
+          '()
+          1
+          partitions)))
 
 (define MS_BIND 4096)                             ; <sys/mounts.h> again!
 
-(define* (format-partition partition type
-                           #:key label)
-  "Create a file system TYPE on PARTITION.  If LABEL is true, use that as the
-volume name."
+(define* (create-ext-file-system partition type
+                                 #:key label)
+  "Create an ext-family filesystem of TYPE on PARTITION.  If LABEL is true,
+use that as the volume name."
   (format #t "creating ~a partition...\n" type)
   (unless (zero? (apply system* (string-append "mkfs." type)
                         "-F" partition
@@ -143,99 +234,206 @@ volume name."
                             '())))
     (error "failed to create partition")))
 
-(define* (initialize-root-partition target-directory
-                                    #:key copy-closures? register-closures?
-                                    closures system-directory)
-  "Initialize the root partition mounted at TARGET-DIRECTORY."
-  (define target-store
-    (string-append target-directory (%store-directory)))
-
-  (when copy-closures?
-    ;; Populate the store.
-    (populate-store (map (cut string-append "/xchg/" <>) closures)
-                    target-directory))
-
-  ;; Populate /dev.
-  (make-essential-device-nodes #:root target-directory)
-
-  ;; Optionally, register the inputs in the image's store.
-  (when register-closures?
-    (unless copy-closures?
-      ;; XXX: 'guix-register' wants to palpate the things it registers, so
-      ;; bind-mount the store on the target.
-      (mkdir-p target-store)
-      (mount (%store-directory) target-store "" MS_BIND))
-
-    (display "registering closures...\n")
-    (for-each (lambda (closure)
-                (register-closure target-directory
-                                  (string-append "/xchg/" closure)))
-              closures)
-    (unless copy-closures?
-      (system* "umount" target-store)))
-
-  ;; Add the non-store directories and files.
-  (display "populating...\n")
-  (populate-root-file-system system-directory target-directory))
-
-(define (register-grub.cfg-root target grub.cfg)
-  "On file system TARGET, register GRUB.CFG as a GC root."
-  (let ((directory (string-append target "/var/guix/gcroots")))
-    (mkdir-p directory)
-    (symlink grub.cfg (string-append directory "/grub.cfg"))))
+(define* (create-fat-file-system partition
+                                 #:key label)
+  "Create a FAT filesystem on PARTITION.  The number of File Allocation Tables
+will be determined based on filesystem size.  If LABEL is true, use that as the
+volume name."
+  (format #t "creating FAT partition...\n")
+  (unless (zero? (apply system* "mkfs.fat" partition
+                        (if label
+                            `("-n" ,label)
+                            '())))
+    (error "failed to create FAT partition")))
 
-(define* (initialize-hard-disk device
-                               #:key
-                               system-directory
-                               grub.cfg
-                               disk-image-size
-                               (file-system-type "ext4")
-                               file-system-label
-                               (closures '())
-                               copy-closures?
-                               (bootable? #t)
-                               (register-closures? #t))
-  "Initialize DEVICE, a disk of DISK-IMAGE-SIZE bytes, with a FILE-SYSTEM-TYPE
-partition with (optionally) FILE-SYSTEM-LABEL as its volume name, and with
-GRUB installed.  When BOOTABLE? is true, set the bootable flag on that
-partition.
+(define* (format-partition partition type
+                           #:key label)
+  "Create a file system TYPE on PARTITION.  If LABEL is true, use that as the
+volume name."
+  (cond ((string-prefix? "ext" type)
+         (create-ext-file-system partition type #:label label))
+        ((or (string-prefix? "fat" type) (string= "vfat" type))
+         (create-fat-file-system partition #:label label))
+        (else (error "Unsupported file system."))))
+
+(define (initialize-partition partition)
+  "Format PARTITION, a <partition> object with a non-#f 'device' field, mount
+it, run its initializer, and unmount it."
+  (let ((target "/fs"))
+   (format-partition (partition-device partition)
+                     (partition-file-system partition)
+                     #:label (partition-label partition))
+   (mkdir-p target)
+   (mount (partition-device partition) target
+          (partition-file-system partition))
+
+   ((partition-initializer partition) target)
+
+   (umount target)
+   partition))
+
+(define* (root-partition-initializer #:key (closures '())
+                                     copy-closures?
+                                     (register-closures? #t)
+                                     system-directory)
+  "Return a procedure to initialize a root partition.
 
 If REGISTER-CLOSURES? is true, register all of CLOSURES is the partition's
 store.  If COPY-CLOSURES? is true, copy all of CLOSURES to the partition.
 SYSTEM-DIRECTORY is the name of the directory of the 'system' derivation."
-  (define target-directory
-    "/fs")
-
-  (define partition
-    (string-append device "1"))
-
-  (initialize-partition-table device
-                              (- disk-image-size (* 5 (expt 2 20)))
-                              #:bootable? bootable?)
-
-  (format-partition partition file-system-type
-                    #:label file-system-label)
-
-  (display "mounting partition...\n")
-  (mkdir target-directory)
-  (mount partition target-directory file-system-type)
-
-  (initialize-root-partition target-directory
-                             #:system-directory system-directory
-                             #:copy-closures? copy-closures?
-                             #:register-closures? register-closures?
-                             #:closures closures)
-
-  (install-grub grub.cfg device target-directory)
-
-  ;; Register GRUB.CFG as a GC root.
-  (register-grub.cfg-root target-directory grub.cfg)
-
-  ;; 'guix-register' resets timestamps and everything, so no need to do it
-  ;; once more in that case.
-  (unless register-closures?
-    (reset-timestamps target-directory))
+  (lambda (target)
+    (define target-store
+      (string-append target (%store-directory)))
+
+    (when copy-closures?
+      ;; Populate the store.
+      (populate-store (map (cut string-append "/xchg/" <>) closures)
+                      target))
+
+    ;; Populate /dev.
+    (make-essential-device-nodes #:root target)
+
+    ;; Optionally, register the inputs in the image's store.
+    (when register-closures?
+      (unless copy-closures?
+        ;; XXX: 'guix-register' wants to palpate the things it registers, so
+        ;; bind-mount the store on the target.
+        (mkdir-p target-store)
+        (mount (%store-directory) target-store "" MS_BIND))
+
+      (display "registering closures...\n")
+      (for-each (lambda (closure)
+                  (register-closure target
+                                    (string-append "/xchg/" closure)))
+                closures)
+      (unless copy-closures?
+        (umount target-store)))
+
+    ;; Add the non-store directories and files.
+    (display "populating...\n")
+    (populate-root-file-system system-directory target)
+
+    ;; 'guix-register' resets timestamps and everything, so no need to do it
+    ;; once more in that case.
+    (unless register-closures?
+      (reset-timestamps target))))
+
+(define (register-bootcfg-root target bootcfg)
+  "On file system TARGET, register BOOTCFG as a GC root."
+  (let ((directory (string-append target "/var/guix/gcroots")))
+    (mkdir-p directory)
+    (symlink bootcfg (string-append directory "/bootcfg"))))
+
+(define (install-efi grub esp config-file)
+  "Write a self-contained GRUB EFI loader to the mounted ESP using CONFIG-FILE."
+  (let* ((system %host-type)
+         ;; Hard code the output location to a well-known path recognized by
+         ;; compliant firmware. See "3.5.1.1 Removable Media Boot Behaviour":
+         ;; http://www.uefi.org/sites/default/files/resources/UEFI%20Spec%202_6.pdf
+         (grub-mkstandalone (string-append grub "/bin/grub-mkstandalone"))
+         (efi-directory (string-append esp "/EFI/BOOT"))
+         ;; Map grub target names to boot file names.
+         (efi-targets (cond ((string-prefix? "x86_64" system)
+                             '("x86_64-efi" . "BOOTX64.EFI"))
+                            ((string-prefix? "i686" system)
+                             '("i386-efi" . "BOOTIA32.EFI"))
+                            ((string-prefix? "armhf" system)
+                             '("arm-efi" . "BOOTARM.EFI"))
+                            ((string-prefix? "aarch64" system)
+                             '("arm64-efi" . "BOOTAA64.EFI")))))
+    ;; grub-mkstandalone requires a TMPDIR to prepare the firmware image.
+    (setenv "TMPDIR" esp)
+
+    (mkdir-p efi-directory)
+    (unless (zero? (system* grub-mkstandalone "-O" (car efi-targets)
+                            "-o" (string-append efi-directory "/"
+                                                (cdr efi-targets))
+                            ;; Graft the configuration file onto the image.
+                            (string-append "boot/grub/grub.cfg=" config-file)))
+      (error "failed to create GRUB EFI image"))))
+
+(define* (make-iso9660-image grub config-file os-drv target
+                             #:key (volume-id "GuixSD"))
+  "Given a GRUB package, creates an iso image as TARGET, using CONFIG-FILE as
+Grub configuration and OS-DRV as the stuff in it."
+  (let ((grub-mkrescue (string-append grub "/bin/grub-mkrescue")))
+    (mkdir-p "/tmp/root/var/run")
+    (mkdir-p "/tmp/root/run")
+    (unless (zero? (system* grub-mkrescue "-o" target
+                            (string-append "boot/grub/grub.cfg=" config-file)
+                            (string-append "gnu/store=" os-drv "/..")
+                            "var=/tmp/root/var"
+                            "run=/tmp/root/run"
+                            "--" "-volid" (string-upcase volume-id)))
+      (error "failed to create ISO image"))))
 
-  (zero? (system* "umount" target-directory)))
+(define* (initialize-hard-disk device
+                               #:key
+                               bootloader-package
+                               bootcfg
+                               bootcfg-location
+                               bootloader-installer
+                               (grub-efi #f)
+                               (partitions '()))
+  "Initialize DEVICE as a disk containing all the <partition> objects listed
+in PARTITIONS, and using BOOTCFG as its bootloader configuration file.
+
+Each partition is initialized by calling its 'initializer' procedure,
+passing it a directory name where it is mounted."
+
+  (define (partition-bootable? partition)
+    "Return the first partition found with the boot flag set."
+    (member 'boot (partition-flags partition)))
+
+  (define (partition-esp? partition)
+    "Return the first EFI System Partition."
+    (member 'esp (partition-flags partition)))
+
+  (let* ((partitions (initialize-partition-table device partitions))
+         (root       (find partition-bootable? partitions))
+         (esp        (find partition-esp? partitions))
+         (target     "/fs"))
+    (unless root
+      (error "no bootable partition specified" partitions))
+
+    (for-each initialize-partition partitions)
+
+    (display "mounting root partition...\n")
+    (mkdir-p target)
+    (mount (partition-device root) target (partition-file-system root))
+    (install-boot-config bootcfg bootcfg-location target)
+    (when bootloader-installer
+      (display "installing bootloader...\n")
+      (bootloader-installer bootloader-package device target))
+
+    (when esp
+      ;; Mount the ESP somewhere and install GRUB UEFI image.
+      (let ((mount-point (string-append target "/boot/efi"))
+            (grub-config (string-append target "/tmp/grub-standalone.cfg")))
+        (display "mounting EFI system partition...\n")
+        (mkdir-p mount-point)
+        (mount (partition-device esp) mount-point
+               (partition-file-system esp))
+
+        ;; Create a tiny configuration file telling the embedded grub
+        ;; where to load the real thing.
+        (call-with-output-file grub-config
+          (lambda (port)
+            (format port
+                    "insmod part_msdos~@
+                    search --set=root --label gnu-disk-image~@
+                    configfile /boot/grub/grub.cfg~%")))
+
+        (display "creating EFI firmware image...")
+        (install-efi grub-efi mount-point grub-config)
+        (display "done.\n")
+
+        (delete-file grub-config)
+        (umount mount-point)))
+
+    ;; Register BOOTCFG as a GC root.
+    (register-bootcfg-root target bootcfg)
+
+    (umount target)))
 
 ;;; vm.scm ends here