vm: Fix 'load-in-linux-vm' docstring.
[jackhill/guix/guix.git] / gnu / build / vm.scm
CommitLineData
e1a87b90 1;;; GNU Guix --- Functional package management for GNU
6efb98ed 2;;; Copyright © 2013, 2014, 2015, 2016, 2017 Ludovic Courtès <ludo@gnu.org>
944d2b17
CAW
3;;; Copyright © 2016 Christopher Allan Webber <cwebber@dustycloud.org>
4;;; Copyright © 2016 Leo Famulari <leo@famulari.name>
07f812c4 5;;; Copyright © 2017 Mathieu Othacehe <m.othacehe@gmail.com>
01cc84da 6;;; Copyright © 2017 Marius Bakke <mbakke@fastmail.com>
e1a87b90
LC
7;;;
8;;; This file is part of GNU Guix.
9;;;
10;;; GNU Guix is free software; you can redistribute it and/or modify it
11;;; under the terms of the GNU General Public License as published by
12;;; the Free Software Foundation; either version 3 of the License, or (at
13;;; your option) any later version.
14;;;
15;;; GNU Guix is distributed in the hope that it will be useful, but
16;;; WITHOUT ANY WARRANTY; without even the implied warranty of
17;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18;;; GNU General Public License for more details.
19;;;
20;;; You should have received a copy of the GNU General Public License
21;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.
22
548f7a8f 23(define-module (gnu build vm)
e1a87b90 24 #:use-module (guix build utils)
6fd1a796 25 #:use-module (guix build store-copy)
abf0880a 26 #:use-module (guix build syscalls)
8a9e21d1 27 #:use-module (gnu build linux-boot)
548f7a8f 28 #:use-module (gnu build install)
72b891e5 29 #:use-module (guix records)
ecf5d537 30 #:use-module (ice-9 format)
55651ff2 31 #:use-module (ice-9 match)
66670cf3 32 #:use-module (ice-9 regex)
72b891e5
LC
33 #:use-module (srfi srfi-1)
34 #:use-module (srfi srfi-9)
55651ff2 35 #:use-module (srfi srfi-26)
66670cf3
LC
36 #:export (qemu-command
37 load-in-linux-vm
641f9a2a 38 format-partition
72b891e5
LC
39
40 partition
41 partition?
42 partition-device
43 partition-size
44 partition-file-system
45 partition-label
01cc84da 46 partition-flags
72b891e5
LC
47 partition-initializer
48
49 root-partition-initializer
641f9a2a 50 initialize-partition-table
55651ff2 51 initialize-hard-disk))
e1a87b90
LC
52
53;;; Commentary:
54;;;
55;;; This module provides supporting code to run virtual machines and build
56;;; virtual machine images using QEMU.
57;;;
58;;; Code:
59
66670cf3
LC
60(define* (qemu-command #:optional (system %host-type))
61 "Return the default name of the QEMU command for SYSTEM."
b1dd6ac5
LC
62 (let ((cpu (substring system 0
63 (string-index system #\-))))
66670cf3
LC
64 (string-append "qemu-system-"
65 (if (string-match "^i[3456]86$" cpu)
66 "i386"
67 cpu))))
e1a87b90
LC
68
69(define* (load-in-linux-vm builder
70 #:key
71 output
72 (qemu (qemu-command)) (memory-size 512)
73 linux initrd
6efb98ed
LC
74 make-disk-image?
75 (disk-image-size (* 100 (expt 2 20)))
c4a74364 76 (disk-image-format "qcow2")
e1a87b90
LC
77 (references-graphs '()))
78 "Run BUILDER, a Scheme file, into a VM running LINUX with INITRD, and copy
79the result to OUTPUT.
80
81When MAKE-DISK-IMAGE? is true, OUTPUT will contain a VM image of
6efb98ed
LC
82DISK-IMAGE-SIZE bytes resulting from the execution of BUILDER, which may
83access it via /dev/hda.
e1a87b90
LC
84
85REFERENCES-GRAPHS can specify a list of reference-graph files as produced by
86the #:references-graphs parameter of 'derivation'."
e1a87b90 87 (when make-disk-image?
c4a74364 88 (unless (zero? (system* "qemu-img" "create" "-f" disk-image-format
d2bcf35e 89 output
e1a87b90
LC
90 (number->string disk-image-size)))
91 (error "qemu-img failed")))
92
93 (mkdir "xchg")
94
95 (match references-graphs
96 ((graph-files ...)
97 ;; Copy the reference-graph files under xchg/ so EXP can access it.
98 (map (lambda (file)
99 (copy-file file (string-append "xchg/" file)))
100 graph-files))
101 (_ #f))
102
103 (unless (zero?
944d2b17 104 (apply system* qemu "-nographic" "-no-reboot"
e1a87b90
LC
105 "-m" (number->string memory-size)
106 "-net" "nic,model=virtio"
107 "-virtfs"
108 (string-append "local,id=store_dev,path="
109 (%store-directory)
110 ",security_model=none,mount_tag=store")
111 "-virtfs"
112 (string-append "local,id=xchg_dev,path=xchg"
113 ",security_model=none,mount_tag=xchg")
114 "-kernel" linux
115 "-initrd" initrd
116 "-append" (string-append "console=ttyS0 --load="
117 builder)
944d2b17
CAW
118 (append
119 (if make-disk-image?
d2bcf35e 120 `("-drive" ,(string-append "file=" output
944d2b17
CAW
121 ",if=virtio"))
122 '())
123 ;; Only enable kvm if we see /dev/kvm exists.
124 ;; This allows users without hardware virtualization to still
125 ;; use these commands.
126 (if (file-exists? "/dev/kvm")
127 '("-enable-kvm")
128 '()))))
e1a87b90
LC
129 (error "qemu failed" qemu))
130
d2bcf35e
LC
131 ;; When MAKE-DISK-IMAGE? is true, the image is in OUTPUT already.
132 (unless make-disk-image?
133 (mkdir output)
134 (copy-recursively "xchg" output)))
e1a87b90 135
72b891e5
LC
136\f
137;;;
138;;; Partitions.
139;;;
140
141(define-record-type* <partition> partition make-partition
142 partition?
143 (device partition-device (default #f))
144 (size partition-size)
145 (file-system partition-file-system (default "ext4"))
146 (label partition-label (default #f))
01cc84da 147 (flags partition-flags (default '()))
72b891e5
LC
148 (initializer partition-initializer (default (const #t))))
149
150(define (fold2 proc seed1 seed2 lst) ;TODO: factorize
151 "Like `fold', but with a single list and two seeds."
152 (let loop ((result1 seed1)
153 (result2 seed2)
154 (lst lst))
155 (if (null? lst)
156 (values result1 result2)
157 (call-with-values
158 (lambda () (proc (car lst) result1 result2))
159 (lambda (result1 result2)
160 (loop result1 result2 (cdr lst)))))))
161
162(define* (initialize-partition-table device partitions
55651ff2
LC
163 #:key
164 (label-type "msdos")
641f9a2a 165 (offset (expt 2 20)))
72b891e5
LC
166 "Create on DEVICE a partition table of type LABEL-TYPE, containing the given
167PARTITIONS (a list of <partition> objects), starting at OFFSET bytes. On
168success, return PARTITIONS with their 'device' field changed to reflect their
169actual /dev name based on DEVICE."
170 (define (partition-options part offset index)
171 (cons* "mkpart" "primary" "ext2"
172 (format #f "~aB" offset)
173 (format #f "~aB" (+ offset (partition-size part)))
01cc84da
MB
174 (append-map (lambda (flag)
175 (list "set" (number->string index)
176 (symbol->string flag) "on"))
177 (partition-flags part))))
72b891e5
LC
178
179 (define (options partitions offset)
180 (let loop ((partitions partitions)
181 (offset offset)
182 (index 1)
183 (result '()))
184 (match partitions
185 (()
186 (concatenate (reverse result)))
187 ((head tail ...)
188 (loop tail
189 ;; Leave one sector (512B) between partitions to placate
190 ;; Parted.
191 (+ offset 512 (partition-size head))
192 (+ 1 index)
193 (cons (partition-options head offset index)
194 result))))))
195
196 (format #t "creating partition table with ~a partitions...\n"
197 (length partitions))
198 (unless (zero? (apply system* "parted" "--script"
199 device "mklabel" label-type
200 (options partitions offset)))
201 (error "failed to create partition table"))
202
203 ;; Set the 'device' field of each partition.
204 (reverse
205 (fold2 (lambda (part result index)
206 (values (cons (partition
207 (inherit part)
208 (device (string-append device
209 (number->string index))))
210 result)
211 (+ 1 index)))
212 '()
213 1
214 partitions)))
55651ff2 215
150e20dd
LC
216(define MS_BIND 4096) ; <sys/mounts.h> again!
217
4d415f0c
MB
218(define* (create-ext-file-system partition type
219 #:key label)
220 "Create an ext-family filesystem of TYPE on PARTITION. If LABEL is true,
221use that as the volume name."
641f9a2a 222 (format #t "creating ~a partition...\n" type)
ef9fc40d
LC
223 (unless (zero? (apply system* (string-append "mkfs." type)
224 "-F" partition
225 (if label
226 `("-L" ,label)
227 '())))
641f9a2a 228 (error "failed to create partition")))
150e20dd 229
4d415f0c
MB
230(define* (create-fat-file-system partition
231 #:key label)
232 "Create a FAT filesystem on PARTITION. The number of File Allocation Tables
233will be determined based on filesystem size. If LABEL is true, use that as the
234volume name."
235 (format #t "creating FAT partition...\n")
236 (unless (zero? (apply system* "mkfs.fat" partition
237 (if label
238 `("-n" ,label)
239 '())))
240 (error "failed to create FAT partition")))
241
242(define* (format-partition partition type
243 #:key label)
244 "Create a file system TYPE on PARTITION. If LABEL is true, use that as the
245volume name."
246 (cond ((string-prefix? "ext" type)
247 (create-ext-file-system partition type #:label label))
248 ((or (string-prefix? "fat" type) (string= "vfat" type))
249 (create-fat-file-system partition #:label label))
250 (else (error "Unsupported file system."))))
251
72b891e5
LC
252(define (initialize-partition partition)
253 "Format PARTITION, a <partition> object with a non-#f 'device' field, mount
254it, run its initializer, and unmount it."
255 (let ((target "/fs"))
256 (format-partition (partition-device partition)
257 (partition-file-system partition)
258 #:label (partition-label partition))
259 (mkdir-p target)
260 (mount (partition-device partition) target
261 (partition-file-system partition))
262
263 ((partition-initializer partition) target)
264
265 (umount target)
266 partition))
267
268(define* (root-partition-initializer #:key (closures '())
269 copy-closures?
270 (register-closures? #t)
271 system-directory)
272 "Return a procedure to initialize a root partition.
273
274If REGISTER-CLOSURES? is true, register all of CLOSURES is the partition's
275store. If COPY-CLOSURES? is true, copy all of CLOSURES to the partition.
276SYSTEM-DIRECTORY is the name of the directory of the 'system' derivation."
277 (lambda (target)
278 (define target-store
279 (string-append target (%store-directory)))
280
281 (when copy-closures?
282 ;; Populate the store.
283 (populate-store (map (cut string-append "/xchg/" <>) closures)
284 target))
285
286 ;; Populate /dev.
287 (make-essential-device-nodes #:root target)
288
289 ;; Optionally, register the inputs in the image's store.
290 (when register-closures?
291 (unless copy-closures?
292 ;; XXX: 'guix-register' wants to palpate the things it registers, so
293 ;; bind-mount the store on the target.
294 (mkdir-p target-store)
295 (mount (%store-directory) target-store "" MS_BIND))
296
297 (display "registering closures...\n")
298 (for-each (lambda (closure)
299 (register-closure target
300 (string-append "/xchg/" closure)))
301 closures)
302 (unless copy-closures?
303 (umount target-store)))
304
305 ;; Add the non-store directories and files.
306 (display "populating...\n")
307 (populate-root-file-system system-directory target)
308
309 ;; 'guix-register' resets timestamps and everything, so no need to do it
310 ;; once more in that case.
311 (unless register-closures?
312 (reset-timestamps target))))
641f9a2a 313
9121ce55 314(define (register-bootcfg-root target bootcfg)
07f812c4 315 "On file system TARGET, register BOOTCFG as a GC root."
6412e58a 316 (let ((directory (string-append target "/var/guix/gcroots")))
39d1f82b 317 (mkdir-p directory)
9121ce55 318 (symlink bootcfg (string-append directory "/bootcfg"))))
39d1f82b 319
ecf5d537
MB
320(define (install-efi grub esp config-file)
321 "Write a self-contained GRUB EFI loader to the mounted ESP using CONFIG-FILE."
322 (let* ((system %host-type)
323 ;; Hard code the output location to a well-known path recognized by
324 ;; compliant firmware. See "3.5.1.1 Removable Media Boot Behaviour":
325 ;; http://www.uefi.org/sites/default/files/resources/UEFI%20Spec%202_6.pdf
326 (grub-mkstandalone (string-append grub "/bin/grub-mkstandalone"))
327 (efi-directory (string-append esp "/EFI/BOOT"))
328 ;; Map grub target names to boot file names.
329 (efi-targets (cond ((string-prefix? "x86_64" system)
330 '("x86_64-efi" . "BOOTX64.EFI"))
331 ((string-prefix? "i686" system)
332 '("i386-efi" . "BOOTIA32.EFI"))
333 ((string-prefix? "armhf" system)
334 '("arm-efi" . "BOOTARM.EFI"))
335 ((string-prefix? "aarch64" system)
336 '("arm64-efi" . "BOOTAA64.EFI")))))
337 ;; grub-mkstandalone requires a TMPDIR to prepare the firmware image.
338 (setenv "TMPDIR" esp)
339
340 (mkdir-p efi-directory)
341 (unless (zero? (system* grub-mkstandalone "-O" (car efi-targets)
342 "-o" (string-append efi-directory "/"
343 (cdr efi-targets))
344 ;; Graft the configuration file onto the image.
345 (string-append "boot/grub/grub.cfg=" config-file)))
346 (error "failed to create GRUB EFI image"))))
347
641f9a2a
LC
348(define* (initialize-hard-disk device
349 #:key
9121ce55
MO
350 bootloader-package
351 bootcfg
352 bootcfg-location
353 bootloader-installer
ecf5d537 354 (grub-efi #f)
72b891e5
LC
355 (partitions '()))
356 "Initialize DEVICE as a disk containing all the <partition> objects listed
07f812c4 357in PARTITIONS, and using BOOTCFG as its bootloader configuration file.
641f9a2a 358
72b891e5
LC
359Each partition is initialized by calling its 'initializer' procedure,
360passing it a directory name where it is mounted."
01cc84da
MB
361
362 (define (partition-bootable? partition)
363 "Return the first partition found with the boot flag set."
364 (member 'boot (partition-flags partition)))
365
ecf5d537
MB
366 (define (partition-esp? partition)
367 "Return the first EFI System Partition."
368 (member 'esp (partition-flags partition)))
369
72b891e5
LC
370 (let* ((partitions (initialize-partition-table device partitions))
371 (root (find partition-bootable? partitions))
ecf5d537 372 (esp (find partition-esp? partitions))
72b891e5
LC
373 (target "/fs"))
374 (unless root
375 (error "no bootable partition specified" partitions))
55651ff2 376
72b891e5 377 (for-each initialize-partition partitions)
55651ff2 378
72b891e5
LC
379 (display "mounting root partition...\n")
380 (mkdir-p target)
381 (mount (partition-device root) target (partition-file-system root))
9121ce55
MO
382 (install-boot-config bootcfg bootcfg-location target)
383 (when bootloader-installer
ecf5d537 384 (display "installing bootloader...\n")
9121ce55 385 (bootloader-installer bootloader-package device target))
39d1f82b 386
ecf5d537
MB
387 (when esp
388 ;; Mount the ESP somewhere and install GRUB UEFI image.
389 (let ((mount-point (string-append target "/boot/efi"))
390 (grub-config (string-append target "/tmp/grub-standalone.cfg")))
391 (display "mounting EFI system partition...\n")
392 (mkdir-p mount-point)
393 (mount (partition-device esp) mount-point
394 (partition-file-system esp))
395
396 ;; Create a tiny configuration file telling the embedded grub
397 ;; where to load the real thing.
398 (call-with-output-file grub-config
399 (lambda (port)
400 (format port
401 "insmod part_msdos~@
402 search --set=root --label gnu-disk-image~@
403 configfile /boot/grub/grub.cfg~%")))
404
405 (display "creating EFI firmware image...")
406 (install-efi grub-efi mount-point grub-config)
407 (display "done.\n")
408
409 (delete-file grub-config)
410 (umount mount-point)))
411
9121ce55
MO
412 ;; Register BOOTCFG as a GC root.
413 (register-bootcfg-root target bootcfg)
55651ff2 414
72b891e5 415 (umount target)))
55651ff2 416
e1a87b90 417;;; vm.scm ends here