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