system: Support activation service for the Hurd.
[jackhill/guix/guix.git] / gnu / build / vm.scm
CommitLineData
e1a87b90 1;;; GNU Guix --- Functional package management for GNU
be6520e6 2;;; Copyright © 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020 Ludovic Courtès <ludo@gnu.org>
944d2b17 3;;; Copyright © 2016 Christopher Allan Webber <cwebber@dustycloud.org>
2ca712bd 4;;; Copyright © 2016, 2017 Leo Famulari <leo@famulari.name>
07f812c4 5;;; Copyright © 2017 Mathieu Othacehe <m.othacehe@gmail.com>
01cc84da 6;;; Copyright © 2017 Marius Bakke <mbakke@fastmail.com>
af81311b 7;;; Copyright © 2018 Chris Marusich <cmmarusich@gmail.com>
26c1bd9d 8;;; Copyright © 2020 Tobias Geerinckx-Rice <me@tobias.gr>
e1a87b90
LC
9;;;
10;;; This file is part of GNU Guix.
11;;;
12;;; GNU Guix is free software; you can redistribute it and/or modify it
13;;; under the terms of the GNU General Public License as published by
14;;; the Free Software Foundation; either version 3 of the License, or (at
15;;; your option) any later version.
16;;;
17;;; GNU Guix is distributed in the hope that it will be useful, but
18;;; WITHOUT ANY WARRANTY; without even the implied warranty of
19;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20;;; GNU General Public License for more details.
21;;;
22;;; You should have received a copy of the GNU General Public License
23;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.
24
548f7a8f 25(define-module (gnu build vm)
e1a87b90 26 #:use-module (guix build utils)
6fd1a796 27 #:use-module (guix build store-copy)
abf0880a 28 #:use-module (guix build syscalls)
b27ef1d4 29 #:use-module (guix store database)
f8fd1157 30 #:use-module (gnu build bootloader)
8a9e21d1 31 #:use-module (gnu build linux-boot)
548f7a8f 32 #:use-module (gnu build install)
47cef4ec 33 #:use-module (gnu system uuid)
72b891e5 34 #:use-module (guix records)
a2278922 35 #:use-module ((guix combinators) #:select (fold2))
ecf5d537 36 #:use-module (ice-9 format)
6a488a35 37 #:use-module (ice-9 ftw)
55651ff2 38 #:use-module (ice-9 match)
66670cf3 39 #:use-module (ice-9 regex)
718d44cc 40 #:use-module (ice-9 popen)
72b891e5
LC
41 #:use-module (srfi srfi-1)
42 #:use-module (srfi srfi-9)
6901b924 43 #:use-module (srfi srfi-19)
55651ff2 44 #:use-module (srfi srfi-26)
66670cf3
LC
45 #:export (qemu-command
46 load-in-linux-vm
641f9a2a 47 format-partition
72b891e5
LC
48
49 partition
50 partition?
51 partition-device
52 partition-size
53 partition-file-system
54 partition-label
01cc84da 55 partition-flags
72b891e5
LC
56 partition-initializer
57
a8ac4f08 58 estimated-partition-size
72b891e5 59 root-partition-initializer
641f9a2a 60 initialize-partition-table
77f52962 61 initialize-hard-disk))
e1a87b90
LC
62
63;;; Commentary:
64;;;
65;;; This module provides supporting code to run virtual machines and build
66;;; virtual machine images using QEMU.
67;;;
68;;; Code:
69
66670cf3
LC
70(define* (qemu-command #:optional (system %host-type))
71 "Return the default name of the QEMU command for SYSTEM."
b1dd6ac5
LC
72 (let ((cpu (substring system 0
73 (string-index system #\-))))
66670cf3 74 (string-append "qemu-system-"
c6d13063
MO
75 (cond
76 ((string-match "^i[3456]86$" cpu) "i386")
77 ((string-match "armhf" cpu) "arm")
78 (else cpu)))))
e1a87b90
LC
79
80(define* (load-in-linux-vm builder
81 #:key
82 output
83 (qemu (qemu-command)) (memory-size 512)
84 linux initrd
6efb98ed 85 make-disk-image?
8d033e3e 86 single-file-output?
6efb98ed 87 (disk-image-size (* 100 (expt 2 20)))
c4a74364 88 (disk-image-format "qcow2")
e1a87b90
LC
89 (references-graphs '()))
90 "Run BUILDER, a Scheme file, into a VM running LINUX with INITRD, and copy
8d033e3e
LC
91the result to OUTPUT. If SINGLE-FILE-OUTPUT? is true, copy a single file from
92/xchg to OUTPUT. Otherwise, copy the contents of /xchg to a new directory
93OUTPUT.
e1a87b90
LC
94
95When MAKE-DISK-IMAGE? is true, OUTPUT will contain a VM image of
6efb98ed
LC
96DISK-IMAGE-SIZE bytes resulting from the execution of BUILDER, which may
97access it via /dev/hda.
e1a87b90
LC
98
99REFERENCES-GRAPHS can specify a list of reference-graph files as produced by
100the #:references-graphs parameter of 'derivation'."
acf54bca 101
b3477234
LC
102 (define target-arm32?
103 (string-prefix? "arm-" %host-type))
104
105 (define target-aarch64?
106 (string-prefix? "aarch64-" %host-type))
107
108 (define target-arm?
109 (or target-arm32? target-aarch64?))
1ee72bb5 110
acf54bca
MO
111 (define arch-specific-flags
112 `(;; On ARM, a machine has to be specified. Use "virt" machine to avoid
113 ;; hardware limits imposed by other machines.
1ee72bb5
MO
114 ,@(if target-arm?
115 '("-M" "virt")
116 '())
acf54bca 117
2608417a
MO
118 ;; On ARM32, if the kernel is built without LPAE support, ECAM conflicts
119 ;; with VIRT_PCIE_MMIO causing PCI devices not to show up. Disable
120 ;; explicitely highmem to fix it.
121 ;; See: https://bugs.launchpad.net/qemu/+bug/1790975.
122 ,@(if target-arm32?
123 '("-machine" "highmem=off")
124 '())
125
acf54bca
MO
126 ;; Only enable kvm if we see /dev/kvm exists. This allows users without
127 ;; hardware virtualization to still use these commands. KVM support is
1ee72bb5 128 ;; still buggy on some ARM boards. Do not use it even if available.
acf54bca 129 ,@(if (and (file-exists? "/dev/kvm")
1ee72bb5 130 (not target-arm?))
acf54bca
MO
131 '("-enable-kvm")
132 '())
98e0b128
LC
133
134 ;; Pass "panic=1" so that the guest dies upon error.
acf54bca 135 "-append"
98e0b128
LC
136 ,(string-append "panic=1 --load=" builder
137
138 ;; The serial port name differs between emulated
139 ;; architectures/machines.
140 " console="
8e53fe2b 141 (if target-arm? "ttyAMA0" "ttyS0"))))
acf54bca 142
e1a87b90 143 (when make-disk-image?
a2cf57e7
LC
144 (format #t "creating ~a image of ~,2f MiB...~%"
145 disk-image-format (/ disk-image-size (expt 2 20)))
146 (force-output)
e1d0f2aa
LC
147 (invoke "qemu-img" "create" "-f" disk-image-format output
148 (number->string disk-image-size)))
e1a87b90
LC
149
150 (mkdir "xchg")
8c9bf294 151 (mkdir "tmp")
e1a87b90
LC
152
153 (match references-graphs
154 ((graph-files ...)
155 ;; Copy the reference-graph files under xchg/ so EXP can access it.
156 (map (lambda (file)
157 (copy-file file (string-append "xchg/" file)))
158 graph-files))
159 (_ #f))
160
e1d0f2aa 161 (apply invoke qemu "-nographic" "-no-reboot"
1ee72bb5
MO
162 ;; CPU "max" behaves as "host" when KVM is enabled, and like a system
163 ;; CPU with the maximum possible feature set otherwise.
164 "-cpu" "max"
e1d0f2aa 165 "-m" (number->string memory-size)
8e53fe2b 166 "-nic" "user,model=virtio-net-pci"
e1d0f2aa
LC
167 "-object" "rng-random,filename=/dev/urandom,id=guixsd-vm-rng"
168 "-device" "virtio-rng-pci,rng=guixsd-vm-rng"
169 "-virtfs"
170 (string-append "local,id=store_dev,path="
171 (%store-directory)
172 ",security_model=none,mount_tag=store")
173 "-virtfs"
174 (string-append "local,id=xchg_dev,path=xchg"
175 ",security_model=none,mount_tag=xchg")
8c9bf294
CM
176 "-virtfs"
177 ;; Some programs require more space in /tmp than is normally
178 ;; available in the guest. Accommodate such programs by sharing a
179 ;; temporary directory.
180 (string-append "local,id=tmp_dev,path=tmp"
181 ",security_model=none,mount_tag=tmp")
e1d0f2aa
LC
182 "-kernel" linux
183 "-initrd" initrd
e1d0f2aa
LC
184 (append
185 (if make-disk-image?
186 `("-device" "virtio-blk,drive=myhd"
187 "-drive" ,(string-append "if=none,file=" output
188 ",format=" disk-image-format
189 ",id=myhd"))
190 '())
191 arch-specific-flags))
e1a87b90 192
be6520e6
LC
193 (unless (file-exists? "xchg/.exit-status")
194 (error "VM did not produce an exit code"))
195
196 (match (call-with-input-file "xchg/.exit-status" read)
197 (0 #t)
198 (status (error "guest VM code exited with a non-zero status" status)))
199
200 (delete-file "xchg/.exit-status")
201
d2bcf35e
LC
202 ;; When MAKE-DISK-IMAGE? is true, the image is in OUTPUT already.
203 (unless make-disk-image?
8d033e3e
LC
204 (if single-file-output?
205 (let ((graph? (lambda (name stat)
206 (member (basename name) references-graphs))))
207 (match (find-files "xchg" (negate graph?))
208 ((result)
209 (copy-file result output))
210 (x
211 (error "did not find a single result file" x))))
212 (begin
213 (mkdir output)
214 (copy-recursively "xchg" output)))))
e1a87b90 215
b27ef1d4
LC
216(define* (register-closure prefix closure
217 #:key
218 (deduplicate? #t) (reset-timestamps? #t)
219 (schema (sql-schema)))
220 "Register CLOSURE in PREFIX, where PREFIX is the directory name of the
221target store and CLOSURE is the name of a file containing a reference graph as
222produced by #:references-graphs.. As a side effect, if RESET-TIMESTAMPS? is
223true, reset timestamps on store files and, if DEDUPLICATE? is true,
224deduplicates files common to CLOSURE and the rest of PREFIX."
225 (let ((items (call-with-input-file closure read-reference-graph)))
226 (register-items items
227 #:prefix prefix
228 #:deduplicate? deduplicate?
229 #:reset-timestamps? reset-timestamps?
230 #:registration-time %epoch
231 #:schema schema)))
232
72b891e5
LC
233\f
234;;;
235;;; Partitions.
236;;;
237
238(define-record-type* <partition> partition make-partition
239 partition?
240 (device partition-device (default #f))
241 (size partition-size)
242 (file-system partition-file-system (default "ext4"))
4d1ff68d
LC
243 (file-system-options partition-file-system-options ;passed to 'mkfs.FS'
244 (default '()))
72b891e5 245 (label partition-label (default #f))
bae28ccb 246 (uuid partition-uuid (default #f))
01cc84da 247 (flags partition-flags (default '()))
72b891e5
LC
248 (initializer partition-initializer (default (const #t))))
249
a8ac4f08
LC
250(define (estimated-partition-size graphs)
251 "Return the estimated size of a partition that can store the store items
252given by GRAPHS, a list of file names produced by #:references-graphs."
21ffcd65
TGR
253 ;; Simply add a 25% overhead.
254 (round (* 1.25 (closure-size graphs))))
a8ac4f08 255
72b891e5 256(define* (initialize-partition-table device partitions
55651ff2
LC
257 #:key
258 (label-type "msdos")
641f9a2a 259 (offset (expt 2 20)))
72b891e5
LC
260 "Create on DEVICE a partition table of type LABEL-TYPE, containing the given
261PARTITIONS (a list of <partition> objects), starting at OFFSET bytes. On
262success, return PARTITIONS with their 'device' field changed to reflect their
263actual /dev name based on DEVICE."
264 (define (partition-options part offset index)
265 (cons* "mkpart" "primary" "ext2"
266 (format #f "~aB" offset)
267 (format #f "~aB" (+ offset (partition-size part)))
01cc84da
MB
268 (append-map (lambda (flag)
269 (list "set" (number->string index)
270 (symbol->string flag) "on"))
271 (partition-flags part))))
72b891e5
LC
272
273 (define (options partitions offset)
274 (let loop ((partitions partitions)
275 (offset offset)
276 (index 1)
277 (result '()))
278 (match partitions
279 (()
280 (concatenate (reverse result)))
281 ((head tail ...)
282 (loop tail
283 ;; Leave one sector (512B) between partitions to placate
284 ;; Parted.
285 (+ offset 512 (partition-size head))
286 (+ 1 index)
287 (cons (partition-options head offset index)
288 result))))))
289
a2cf57e7
LC
290 (format #t "creating partition table with ~a partitions (~a)...\n"
291 (length partitions)
292 (string-join (map (compose (cut string-append <> " MiB")
293 number->string
294 (lambda (size)
295 (round (/ size (expt 2. 20))))
296 partition-size)
297 partitions)
298 ", "))
e1d0f2aa
LC
299 (apply invoke "parted" "--script"
300 device "mklabel" label-type
301 (options partitions offset))
72b891e5
LC
302
303 ;; Set the 'device' field of each partition.
304 (reverse
305 (fold2 (lambda (part result index)
306 (values (cons (partition
307 (inherit part)
308 (device (string-append device
309 (number->string index))))
310 result)
311 (+ 1 index)))
312 '()
313 1
314 partitions)))
55651ff2 315
150e20dd
LC
316(define MS_BIND 4096) ; <sys/mounts.h> again!
317
4d415f0c 318(define* (create-ext-file-system partition type
4d1ff68d 319 #:key label uuid (options '()))
162a1374 320 "Create an ext-family file system of TYPE on PARTITION. If LABEL is true,
bae28ccb 321use that as the volume name. If UUID is true, use it as the partition UUID."
353df401
LC
322 (format #t "creating ~a partition... ~@[label: ~s~] ~@[uuid: ~s~]\n"
323 type label (and uuid (uuid->string uuid)))
e1d0f2aa
LC
324 (apply invoke (string-append "mkfs." type)
325 "-F" partition
326 `(,@(if label
327 `("-L" ,label)
328 '())
329 ,@(if uuid
330 `("-U" ,(uuid->string uuid))
4d1ff68d
LC
331 '())
332 ,@options)))
150e20dd 333
4d415f0c 334(define* (create-fat-file-system partition
4d1ff68d 335 #:key label uuid (options '()))
162a1374
TGR
336 "Create a FAT file system on PARTITION. The number of File Allocation Tables
337will be determined based on file system size. If LABEL is true, use that as the
4d415f0c 338volume name."
bae28ccb 339 ;; FIXME: UUID is ignored!
4d415f0c 340 (format #t "creating FAT partition...\n")
e1d0f2aa 341 (apply invoke "mkfs.fat" partition
4d1ff68d 342 (append (if label `("-n" ,label) '()) options)))
4d415f0c
MB
343
344(define* (format-partition partition type
4d1ff68d 345 #:key label uuid (options '()))
4d415f0c 346 "Create a file system TYPE on PARTITION. If LABEL is true, use that as the
4d1ff68d 347volume name. Options is a list of command-line options passed to 'mkfs.FS'."
4d415f0c 348 (cond ((string-prefix? "ext" type)
4d1ff68d
LC
349 (create-ext-file-system partition type #:label label #:uuid uuid
350 #:options options))
4d415f0c 351 ((or (string-prefix? "fat" type) (string= "vfat" type))
4d1ff68d
LC
352 (create-fat-file-system partition #:label label #:uuid uuid
353 #:options options))
4d415f0c
MB
354 (else (error "Unsupported file system."))))
355
72b891e5
LC
356(define (initialize-partition partition)
357 "Format PARTITION, a <partition> object with a non-#f 'device' field, mount
358it, run its initializer, and unmount it."
359 (let ((target "/fs"))
360 (format-partition (partition-device partition)
361 (partition-file-system partition)
bae28ccb 362 #:label (partition-label partition)
4d1ff68d
LC
363 #:uuid (partition-uuid partition)
364 #:options (partition-file-system-options partition))
72b891e5
LC
365 (mkdir-p target)
366 (mount (partition-device partition) target
367 (partition-file-system partition))
368
369 ((partition-initializer partition) target)
370
371 (umount target)
372 partition))
373
374(define* (root-partition-initializer #:key (closures '())
375 copy-closures?
376 (register-closures? #t)
af81311b 377 system-directory
82782d8c 378 (deduplicate? #t)
cd45d656
LC
379 (make-device-nodes
380 make-essential-device-nodes)
82782d8c 381 (extra-directives '()))
72b891e5
LC
382 "Return a procedure to initialize a root partition.
383
af81311b
CM
384If REGISTER-CLOSURES? is true, register all of CLOSURES in the partition's
385store. If DEDUPLICATE? is true, then also deduplicate files common to
386CLOSURES and the rest of the store when registering the closures. If
387COPY-CLOSURES? is true, copy all of CLOSURES to the partition.
82782d8c
LC
388SYSTEM-DIRECTORY is the name of the directory of the 'system' derivation.
389
390EXTRA-DIRECTIVES is an optional list of directives to populate the root file
391system that is passed to 'populate-root-file-system'."
72b891e5
LC
392 (lambda (target)
393 (define target-store
394 (string-append target (%store-directory)))
395
396 (when copy-closures?
397 ;; Populate the store.
398 (populate-store (map (cut string-append "/xchg/" <>) closures)
399 target))
400
401 ;; Populate /dev.
cd45d656 402 (make-device-nodes target)
72b891e5
LC
403
404 ;; Optionally, register the inputs in the image's store.
405 (when register-closures?
406 (unless copy-closures?
ea0a06ce 407 ;; XXX: 'register-closure' wants to palpate the things it registers, so
72b891e5
LC
408 ;; bind-mount the store on the target.
409 (mkdir-p target-store)
410 (mount (%store-directory) target-store "" MS_BIND))
411
412 (display "registering closures...\n")
413 (for-each (lambda (closure)
414 (register-closure target
af81311b 415 (string-append "/xchg/" closure)
c45477d2 416 #:reset-timestamps? copy-closures?
af81311b 417 #:deduplicate? deduplicate?))
72b891e5
LC
418 closures)
419 (unless copy-closures?
420 (umount target-store)))
421
422 ;; Add the non-store directories and files.
423 (display "populating...\n")
82782d8c
LC
424 (populate-root-file-system system-directory target
425 #:extras extra-directives)
72b891e5 426
ea0a06ce 427 ;; 'register-closure' resets timestamps and everything, so no need to do it
72b891e5
LC
428 ;; once more in that case.
429 (unless register-closures?
6a488a35
LC
430 ;; 'reset-timestamps' also resets file permissions; do that everywhere
431 ;; except on /dev so that /dev/null remains writable, etc.
432 (for-each (lambda (directory)
433 (reset-timestamps (string-append target "/" directory)))
434 (scandir target
435 (match-lambda
436 ((or "." ".." "dev") #f)
437 (_ #t))))
438 (reset-timestamps (string-append target "/dev")
439 #:preserve-permissions? #t))))
641f9a2a 440
9121ce55 441(define (register-bootcfg-root target bootcfg)
07f812c4 442 "On file system TARGET, register BOOTCFG as a GC root."
6412e58a 443 (let ((directory (string-append target "/var/guix/gcroots")))
39d1f82b 444 (mkdir-p directory)
9121ce55 445 (symlink bootcfg (string-append directory "/bootcfg"))))
39d1f82b 446
641f9a2a
LC
447(define* (initialize-hard-disk device
448 #:key
9121ce55
MO
449 bootloader-package
450 bootcfg
451 bootcfg-location
452 bootloader-installer
ecf5d537 453 (grub-efi #f)
72b891e5
LC
454 (partitions '()))
455 "Initialize DEVICE as a disk containing all the <partition> objects listed
07f812c4 456in PARTITIONS, and using BOOTCFG as its bootloader configuration file.
641f9a2a 457
72b891e5
LC
458Each partition is initialized by calling its 'initializer' procedure,
459passing it a directory name where it is mounted."
01cc84da
MB
460
461 (define (partition-bootable? partition)
462 "Return the first partition found with the boot flag set."
463 (member 'boot (partition-flags partition)))
464
ecf5d537
MB
465 (define (partition-esp? partition)
466 "Return the first EFI System Partition."
467 (member 'esp (partition-flags partition)))
468
72b891e5
LC
469 (let* ((partitions (initialize-partition-table device partitions))
470 (root (find partition-bootable? partitions))
ecf5d537 471 (esp (find partition-esp? partitions))
72b891e5
LC
472 (target "/fs"))
473 (unless root
474 (error "no bootable partition specified" partitions))
55651ff2 475
72b891e5 476 (for-each initialize-partition partitions)
55651ff2 477
72b891e5
LC
478 (display "mounting root partition...\n")
479 (mkdir-p target)
480 (mount (partition-device root) target (partition-file-system root))
9121ce55
MO
481 (install-boot-config bootcfg bootcfg-location target)
482 (when bootloader-installer
ecf5d537 483 (display "installing bootloader...\n")
9121ce55 484 (bootloader-installer bootloader-package device target))
39d1f82b 485
ecf5d537
MB
486 (when esp
487 ;; Mount the ESP somewhere and install GRUB UEFI image.
f8fd1157 488 (let ((mount-point (string-append target "/boot/efi")))
ecf5d537
MB
489 (display "mounting EFI system partition...\n")
490 (mkdir-p mount-point)
491 (mount (partition-device esp) mount-point
492 (partition-file-system esp))
493
ecf5d537 494 (display "creating EFI firmware image...")
f8fd1157 495 (install-efi-loader grub-efi mount-point)
ecf5d537
MB
496 (display "done.\n")
497
ecf5d537
MB
498 (umount mount-point)))
499
9121ce55
MO
500 ;; Register BOOTCFG as a GC root.
501 (register-bootcfg-root target bootcfg)
55651ff2 502
72b891e5 503 (umount target)))
55651ff2 504
e1a87b90 505;;; vm.scm ends here