Merge branch 'ungrafting' into staging
[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
b27ef1d4
LC
218 (schema (sql-schema)))
219 "Register CLOSURE in PREFIX, where PREFIX is the directory name of the
220target store and CLOSURE is the name of a file containing a reference graph as
2aa512ec 221produced by #:references-graphs."
b27ef1d4 222 (let ((items (call-with-input-file closure read-reference-graph)))
97a46055
LC
223 (parameterize ((sql-schema schema))
224 (with-database (store-database-file #:prefix prefix) db
225 (register-items db items
226 #:prefix prefix
97a46055 227 #:registration-time %epoch)))))
b27ef1d4 228
72b891e5
LC
229\f
230;;;
231;;; Partitions.
232;;;
233
234(define-record-type* <partition> partition make-partition
235 partition?
236 (device partition-device (default #f))
237 (size partition-size)
238 (file-system partition-file-system (default "ext4"))
4d1ff68d
LC
239 (file-system-options partition-file-system-options ;passed to 'mkfs.FS'
240 (default '()))
72b891e5 241 (label partition-label (default #f))
bae28ccb 242 (uuid partition-uuid (default #f))
01cc84da 243 (flags partition-flags (default '()))
72b891e5
LC
244 (initializer partition-initializer (default (const #t))))
245
a8ac4f08
LC
246(define (estimated-partition-size graphs)
247 "Return the estimated size of a partition that can store the store items
248given by GRAPHS, a list of file names produced by #:references-graphs."
21ffcd65
TGR
249 ;; Simply add a 25% overhead.
250 (round (* 1.25 (closure-size graphs))))
a8ac4f08 251
72b891e5 252(define* (initialize-partition-table device partitions
55651ff2
LC
253 #:key
254 (label-type "msdos")
641f9a2a 255 (offset (expt 2 20)))
72b891e5
LC
256 "Create on DEVICE a partition table of type LABEL-TYPE, containing the given
257PARTITIONS (a list of <partition> objects), starting at OFFSET bytes. On
258success, return PARTITIONS with their 'device' field changed to reflect their
259actual /dev name based on DEVICE."
260 (define (partition-options part offset index)
261 (cons* "mkpart" "primary" "ext2"
262 (format #f "~aB" offset)
263 (format #f "~aB" (+ offset (partition-size part)))
01cc84da
MB
264 (append-map (lambda (flag)
265 (list "set" (number->string index)
266 (symbol->string flag) "on"))
267 (partition-flags part))))
72b891e5
LC
268
269 (define (options partitions offset)
270 (let loop ((partitions partitions)
271 (offset offset)
272 (index 1)
273 (result '()))
274 (match partitions
275 (()
276 (concatenate (reverse result)))
277 ((head tail ...)
278 (loop tail
279 ;; Leave one sector (512B) between partitions to placate
280 ;; Parted.
281 (+ offset 512 (partition-size head))
282 (+ 1 index)
283 (cons (partition-options head offset index)
284 result))))))
285
a2cf57e7
LC
286 (format #t "creating partition table with ~a partitions (~a)...\n"
287 (length partitions)
288 (string-join (map (compose (cut string-append <> " MiB")
289 number->string
290 (lambda (size)
291 (round (/ size (expt 2. 20))))
292 partition-size)
293 partitions)
294 ", "))
e1d0f2aa
LC
295 (apply invoke "parted" "--script"
296 device "mklabel" label-type
297 (options partitions offset))
72b891e5
LC
298
299 ;; Set the 'device' field of each partition.
300 (reverse
301 (fold2 (lambda (part result index)
302 (values (cons (partition
303 (inherit part)
304 (device (string-append device
305 (number->string index))))
306 result)
307 (+ 1 index)))
308 '()
309 1
310 partitions)))
55651ff2 311
150e20dd
LC
312(define MS_BIND 4096) ; <sys/mounts.h> again!
313
4d415f0c 314(define* (create-ext-file-system partition type
4d1ff68d 315 #:key label uuid (options '()))
162a1374 316 "Create an ext-family file system of TYPE on PARTITION. If LABEL is true,
bae28ccb 317use that as the volume name. If UUID is true, use it as the partition UUID."
353df401
LC
318 (format #t "creating ~a partition... ~@[label: ~s~] ~@[uuid: ~s~]\n"
319 type label (and uuid (uuid->string uuid)))
e1d0f2aa
LC
320 (apply invoke (string-append "mkfs." type)
321 "-F" partition
322 `(,@(if label
323 `("-L" ,label)
324 '())
325 ,@(if uuid
326 `("-U" ,(uuid->string uuid))
4d1ff68d
LC
327 '())
328 ,@options)))
150e20dd 329
4d415f0c 330(define* (create-fat-file-system partition
4d1ff68d 331 #:key label uuid (options '()))
162a1374
TGR
332 "Create a FAT file system on PARTITION. The number of File Allocation Tables
333will be determined based on file system size. If LABEL is true, use that as the
4d415f0c 334volume name."
bae28ccb 335 ;; FIXME: UUID is ignored!
4d415f0c 336 (format #t "creating FAT partition...\n")
e1d0f2aa 337 (apply invoke "mkfs.fat" partition
4d1ff68d 338 (append (if label `("-n" ,label) '()) options)))
4d415f0c
MB
339
340(define* (format-partition partition type
4d1ff68d 341 #:key label uuid (options '()))
4d415f0c 342 "Create a file system TYPE on PARTITION. If LABEL is true, use that as the
4d1ff68d 343volume name. Options is a list of command-line options passed to 'mkfs.FS'."
4d415f0c 344 (cond ((string-prefix? "ext" type)
4d1ff68d
LC
345 (create-ext-file-system partition type #:label label #:uuid uuid
346 #:options options))
4d415f0c 347 ((or (string-prefix? "fat" type) (string= "vfat" type))
4d1ff68d
LC
348 (create-fat-file-system partition #:label label #:uuid uuid
349 #:options options))
4d415f0c
MB
350 (else (error "Unsupported file system."))))
351
72b891e5
LC
352(define (initialize-partition partition)
353 "Format PARTITION, a <partition> object with a non-#f 'device' field, mount
354it, run its initializer, and unmount it."
355 (let ((target "/fs"))
356 (format-partition (partition-device partition)
357 (partition-file-system partition)
bae28ccb 358 #:label (partition-label partition)
4d1ff68d
LC
359 #:uuid (partition-uuid partition)
360 #:options (partition-file-system-options partition))
72b891e5
LC
361 (mkdir-p target)
362 (mount (partition-device partition) target
363 (partition-file-system partition))
364
365 ((partition-initializer partition) target)
366
367 (umount target)
368 partition))
369
370(define* (root-partition-initializer #:key (closures '())
371 copy-closures?
372 (register-closures? #t)
af81311b 373 system-directory
82782d8c 374 (deduplicate? #t)
cd45d656
LC
375 (make-device-nodes
376 make-essential-device-nodes)
82782d8c 377 (extra-directives '()))
72b891e5
LC
378 "Return a procedure to initialize a root partition.
379
af81311b
CM
380If REGISTER-CLOSURES? is true, register all of CLOSURES in the partition's
381store. If DEDUPLICATE? is true, then also deduplicate files common to
382CLOSURES and the rest of the store when registering the closures. If
383COPY-CLOSURES? is true, copy all of CLOSURES to the partition.
82782d8c
LC
384SYSTEM-DIRECTORY is the name of the directory of the 'system' derivation.
385
386EXTRA-DIRECTIVES is an optional list of directives to populate the root file
387system that is passed to 'populate-root-file-system'."
72b891e5
LC
388 (lambda (target)
389 (define target-store
390 (string-append target (%store-directory)))
391
392 (when copy-closures?
393 ;; Populate the store.
394 (populate-store (map (cut string-append "/xchg/" <>) closures)
6a060ff2
LC
395 target
396 #:deduplicate? deduplicate?))
72b891e5
LC
397
398 ;; Populate /dev.
cd45d656 399 (make-device-nodes target)
72b891e5
LC
400
401 ;; Optionally, register the inputs in the image's store.
402 (when register-closures?
403 (unless copy-closures?
ea0a06ce 404 ;; XXX: 'register-closure' wants to palpate the things it registers, so
72b891e5
LC
405 ;; bind-mount the store on the target.
406 (mkdir-p target-store)
407 (mount (%store-directory) target-store "" MS_BIND))
408
409 (display "registering closures...\n")
410 (for-each (lambda (closure)
411 (register-closure target
2aa512ec 412 (string-append "/xchg/" closure)))
72b891e5
LC
413 closures)
414 (unless copy-closures?
415 (umount target-store)))
416
417 ;; Add the non-store directories and files.
418 (display "populating...\n")
82782d8c
LC
419 (populate-root-file-system system-directory target
420 #:extras extra-directives)
72b891e5 421
ea0a06ce 422 ;; 'register-closure' resets timestamps and everything, so no need to do it
72b891e5
LC
423 ;; once more in that case.
424 (unless register-closures?
6a488a35
LC
425 ;; 'reset-timestamps' also resets file permissions; do that everywhere
426 ;; except on /dev so that /dev/null remains writable, etc.
427 (for-each (lambda (directory)
428 (reset-timestamps (string-append target "/" directory)))
429 (scandir target
430 (match-lambda
431 ((or "." ".." "dev") #f)
432 (_ #t))))
433 (reset-timestamps (string-append target "/dev")
434 #:preserve-permissions? #t))))
641f9a2a 435
9121ce55 436(define (register-bootcfg-root target bootcfg)
07f812c4 437 "On file system TARGET, register BOOTCFG as a GC root."
6412e58a 438 (let ((directory (string-append target "/var/guix/gcroots")))
39d1f82b 439 (mkdir-p directory)
9121ce55 440 (symlink bootcfg (string-append directory "/bootcfg"))))
39d1f82b 441
641f9a2a
LC
442(define* (initialize-hard-disk device
443 #:key
9121ce55
MO
444 bootloader-package
445 bootcfg
446 bootcfg-location
447 bootloader-installer
ecf5d537 448 (grub-efi #f)
72b891e5
LC
449 (partitions '()))
450 "Initialize DEVICE as a disk containing all the <partition> objects listed
07f812c4 451in PARTITIONS, and using BOOTCFG as its bootloader configuration file.
641f9a2a 452
72b891e5
LC
453Each partition is initialized by calling its 'initializer' procedure,
454passing it a directory name where it is mounted."
01cc84da
MB
455
456 (define (partition-bootable? partition)
457 "Return the first partition found with the boot flag set."
458 (member 'boot (partition-flags partition)))
459
ecf5d537
MB
460 (define (partition-esp? partition)
461 "Return the first EFI System Partition."
462 (member 'esp (partition-flags partition)))
463
72b891e5
LC
464 (let* ((partitions (initialize-partition-table device partitions))
465 (root (find partition-bootable? partitions))
ecf5d537 466 (esp (find partition-esp? partitions))
72b891e5
LC
467 (target "/fs"))
468 (unless root
469 (error "no bootable partition specified" partitions))
55651ff2 470
72b891e5 471 (for-each initialize-partition partitions)
55651ff2 472
72b891e5
LC
473 (display "mounting root partition...\n")
474 (mkdir-p target)
475 (mount (partition-device root) target (partition-file-system root))
9121ce55
MO
476 (install-boot-config bootcfg bootcfg-location target)
477 (when bootloader-installer
ecf5d537 478 (display "installing bootloader...\n")
9121ce55 479 (bootloader-installer bootloader-package device target))
39d1f82b 480
ecf5d537
MB
481 (when esp
482 ;; Mount the ESP somewhere and install GRUB UEFI image.
f8fd1157 483 (let ((mount-point (string-append target "/boot/efi")))
ecf5d537
MB
484 (display "mounting EFI system partition...\n")
485 (mkdir-p mount-point)
486 (mount (partition-device esp) mount-point
487 (partition-file-system esp))
488
ecf5d537 489 (display "creating EFI firmware image...")
f8fd1157 490 (install-efi-loader grub-efi mount-point)
ecf5d537
MB
491 (display "done.\n")
492
ecf5d537
MB
493 (umount mount-point)))
494
9121ce55
MO
495 ;; Register BOOTCFG as a GC root.
496 (register-bootcfg-root target bootcfg)
55651ff2 497
72b891e5 498 (umount target)))
55651ff2 499
e1a87b90 500;;; vm.scm ends here