Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
lima-vm
GitHub Repository: lima-vm/lima
Path: blob/master/pkg/driver/qemu/qemu.go
2632 views
1
// SPDX-FileCopyrightText: Copyright The Lima Authors
2
// SPDX-License-Identifier: Apache-2.0
3
4
package qemu
5
6
import (
7
"bytes"
8
"context"
9
"encoding/json"
10
"errors"
11
"fmt"
12
"io"
13
"io/fs"
14
"os"
15
"os/exec"
16
"path/filepath"
17
"regexp"
18
"runtime"
19
"slices"
20
"strconv"
21
"strings"
22
"sync"
23
"time"
24
25
"github.com/coreos/go-semver/semver"
26
"github.com/digitalocean/go-qemu/qmp"
27
"github.com/digitalocean/go-qemu/qmp/raw"
28
"github.com/docker/go-units"
29
"github.com/mattn/go-shellwords"
30
"github.com/sirupsen/logrus"
31
32
"github.com/lima-vm/lima/v2/pkg/fileutils"
33
"github.com/lima-vm/lima/v2/pkg/iso9660util"
34
"github.com/lima-vm/lima/v2/pkg/limatype"
35
"github.com/lima-vm/lima/v2/pkg/limatype/filenames"
36
"github.com/lima-vm/lima/v2/pkg/limayaml"
37
"github.com/lima-vm/lima/v2/pkg/networks"
38
"github.com/lima-vm/lima/v2/pkg/networks/usernet"
39
"github.com/lima-vm/lima/v2/pkg/osutil"
40
"github.com/lima-vm/lima/v2/pkg/qemuimgutil"
41
"github.com/lima-vm/lima/v2/pkg/store"
42
)
43
44
type Config struct {
45
Name string
46
InstanceDir string
47
LimaYAML *limatype.LimaYAML
48
SSHLocalPort int
49
SSHAddress string
50
VirtioGA bool
51
}
52
53
// minimumQemuVersion returns hardMin and softMin.
54
//
55
// hardMin is the hard minimum version of QEMU.
56
// The driver immediately returns the error when QEMU is older than this version.
57
//
58
// softMin is the oldest recommended version of QEMU.
59
// softMin must be >= hardMin.
60
//
61
// When updating this function, make sure to update
62
// `website/content/en/docs/config/vmtype/qemu.md` too.
63
func minimumQemuVersion() (hardMin, softMin semver.Version) {
64
var h, s string
65
switch runtime.GOOS {
66
case "darwin":
67
switch runtime.GOARCH {
68
case "arm64":
69
// https://gitlab.com/qemu-project/qemu/-/issues/1990
70
h, s = "8.2.1", "8.2.1"
71
default:
72
// The code specific to QEMU < 7.0 on macOS (https://github.com/lima-vm/lima/pull/703)
73
// was removed in https://github.com/lima-vm/lima/pull/3491
74
h, s = "7.0.0", "8.2.1"
75
}
76
default:
77
// hardMin: Untested and maybe does not even work.
78
// softMin: Ubuntu 22.04's QEMU. The oldest version that can be easily tested on GitHub Actions.
79
h, s = "4.0.0", "6.2.0"
80
}
81
hardMin, softMin = *semver.New(h), *semver.New(s)
82
if softMin.LessThan(hardMin) {
83
// NOTREACHED
84
logrus.Fatalf("internal error: QEMU: soft minimum version %v must be >= hard minimum version %v",
85
softMin, hardMin)
86
}
87
return hardMin, softMin
88
}
89
90
// EnsureDisk also ensures the kernel and the initrd.
91
func EnsureDisk(ctx context.Context, cfg Config) error {
92
diffDisk := filepath.Join(cfg.InstanceDir, filenames.DiffDisk)
93
if _, err := os.Stat(diffDisk); err == nil || !errors.Is(err, os.ErrNotExist) {
94
// disk is already ensured
95
return err
96
}
97
98
baseDisk := filepath.Join(cfg.InstanceDir, filenames.BaseDisk)
99
100
diskSize, _ := units.RAMInBytes(*cfg.LimaYAML.Disk)
101
if diskSize == 0 {
102
return nil
103
}
104
isBaseDiskISO, err := iso9660util.IsISO9660(baseDisk)
105
if err != nil {
106
return err
107
}
108
baseDiskInfo, err := qemuimgutil.GetInfo(ctx, baseDisk)
109
if err != nil {
110
return fmt.Errorf("failed to get the information of base disk %q: %w", baseDisk, err)
111
}
112
if err = qemuimgutil.AcceptableAsBaseDisk(baseDiskInfo); err != nil {
113
return fmt.Errorf("file %q is not acceptable as the base disk: %w", baseDisk, err)
114
}
115
if baseDiskInfo.Format == "" {
116
return fmt.Errorf("failed to inspect the format of %q", baseDisk)
117
}
118
args := []string{"create", "-f", "qcow2"}
119
if !isBaseDiskISO {
120
args = append(args, "-F", baseDiskInfo.Format, "-b", baseDisk)
121
}
122
args = append(args, diffDisk, strconv.Itoa(int(diskSize)))
123
cmd := exec.CommandContext(ctx, "qemu-img", args...)
124
if out, err := cmd.CombinedOutput(); err != nil {
125
return fmt.Errorf("failed to run %v: %q: %w", cmd.Args, string(out), err)
126
}
127
return nil
128
}
129
130
func newQmpClient(cfg Config) (*qmp.SocketMonitor, error) {
131
qmpSock := filepath.Join(cfg.InstanceDir, filenames.QMPSock)
132
qmpClient, err := qmp.NewSocketMonitor("unix", qmpSock, 5*time.Second)
133
if err != nil {
134
return nil, err
135
}
136
return qmpClient, nil
137
}
138
139
func sendHmpCommand(cfg Config, cmd, tag string) (string, error) {
140
qmpClient, err := newQmpClient(cfg)
141
if err != nil {
142
return "", err
143
}
144
if err := qmpClient.Connect(); err != nil {
145
return "", err
146
}
147
defer func() { _ = qmpClient.Disconnect() }()
148
rawClient := raw.NewMonitor(qmpClient)
149
logrus.Infof("Sending HMP %s command", cmd)
150
hmc := fmt.Sprintf("%s %s", cmd, tag)
151
return rawClient.HumanMonitorCommand(hmc, nil)
152
}
153
154
func execImgCommand(ctx context.Context, cfg Config, args ...string) (string, error) {
155
diffDisk := filepath.Join(cfg.InstanceDir, filenames.DiffDisk)
156
args = append(args, diffDisk)
157
logrus.Debugf("Running qemu-img %v command", args)
158
cmd := exec.CommandContext(ctx, "qemu-img", args...)
159
b, err := cmd.Output()
160
if err != nil {
161
return "", err
162
}
163
return string(b), err
164
}
165
166
func Del(ctx context.Context, cfg Config, run bool, tag string) error {
167
if run {
168
out, err := sendHmpCommand(cfg, "delvm", tag)
169
// there can still be output, even if no error!
170
if out != "" {
171
logrus.Warnf("output: %s", strings.TrimSpace(out))
172
}
173
return err
174
}
175
// -d deletes a snapshot
176
_, err := execImgCommand(ctx, cfg, "snapshot", "-d", tag)
177
return err
178
}
179
180
func Save(ctx context.Context, cfg Config, run bool, tag string) error {
181
if run {
182
out, err := sendHmpCommand(cfg, "savevm", tag)
183
// there can still be output, even if no error!
184
if out != "" {
185
logrus.Warnf("output: %s", strings.TrimSpace(out))
186
}
187
return err
188
}
189
// -c creates a snapshot
190
_, err := execImgCommand(ctx, cfg, "snapshot", "-c", tag)
191
return err
192
}
193
194
func Load(ctx context.Context, cfg Config, run bool, tag string) error {
195
if run {
196
out, err := sendHmpCommand(cfg, "loadvm", tag)
197
// there can still be output, even if no error!
198
if out != "" {
199
logrus.Warnf("output: %s", strings.TrimSpace(out))
200
}
201
return err
202
}
203
// -a applies a snapshot
204
_, err := execImgCommand(ctx, cfg, "snapshot", "-a", tag)
205
return err
206
}
207
208
// List returns a space-separated list of all snapshots, with header and newlines.
209
func List(ctx context.Context, cfg Config, run bool) (string, error) {
210
if run {
211
out, err := sendHmpCommand(cfg, "info", "snapshots")
212
if err == nil {
213
out = strings.ReplaceAll(out, "\r", "")
214
out = strings.Replace(out, "List of snapshots present on all disks:\n", "", 1)
215
out = strings.Replace(out, "There is no snapshot available.\n", "", 1)
216
}
217
return out, err
218
}
219
// -l lists all snapshots
220
args := []string{"snapshot", "-l"}
221
out, err := execImgCommand(ctx, cfg, args...)
222
if err == nil {
223
// remove the redundant heading, result is not machine-parseable
224
out = strings.Replace(out, "Snapshot list:\n", "", 1)
225
}
226
return out, err
227
}
228
229
func argValue(args []string, key string) (string, bool) {
230
if !strings.HasPrefix(key, "-") {
231
panic(fmt.Errorf("got unexpected key %q", key))
232
}
233
for i, s := range args {
234
if s == key {
235
if i == len(args)-1 {
236
return "", true
237
}
238
value := args[i+1]
239
if strings.HasPrefix(value, "-") {
240
return "", true
241
}
242
return value, true
243
}
244
}
245
return "", false
246
}
247
248
// appendArgsIfNoConflict can be used for: -cpu, -machine, -m, -boot ...
249
// appendArgsIfNoConflict cannot be used for: -drive, -cdrom, ...
250
func appendArgsIfNoConflict(args []string, k, v string) []string {
251
if !strings.HasPrefix(k, "-") {
252
panic(fmt.Errorf("got unexpected key %q", k))
253
}
254
switch k {
255
case "-drive", "-cdrom", "-chardev", "-blockdev", "-netdev", "-device":
256
panic(fmt.Errorf("appendArgsIfNoConflict() must not be called with k=%q", k))
257
}
258
259
if v == "" {
260
if _, ok := argValue(args, k); ok {
261
return args
262
}
263
return append(args, k)
264
}
265
266
if origV, ok := argValue(args, k); ok {
267
logrus.Warnf("Not adding QEMU argument %q %q, as it conflicts with %q %q", k, v, k, origV)
268
return args
269
}
270
return append(args, k, v)
271
}
272
273
type features struct {
274
// AccelHelp is the output of `qemu-system-x86_64 -accel help`
275
// e.g. "Accelerators supported in QEMU binary:\ntcg\nhax\nhvf\n"
276
// Not machine-readable, but checking strings.Contains() should be fine.
277
AccelHelp []byte
278
// NetdevHelp is the output of `qemu-system-x86_64 -netdev help`
279
// e.g. "Available netdev backend types:\nsocket\nhubport\ntap\nuser\nvde\nbridge\vhost-user\n"
280
// Not machine-readable, but checking strings.Contains() should be fine.
281
NetdevHelp []byte
282
// MachineHelp is the output of `qemu-system-x86_64 -machine help`
283
// e.g. "Supported machines are:\nakita...\n...virt-6.2...\n...virt-7.0...\n...\n"
284
// Not machine-readable, but checking strings.Contains() should be fine.
285
MachineHelp []byte
286
// CPUHelp is the output of `qemu-system-x86_64 -cpu help`
287
// e.g. "Available CPUs:\n...\nx86 base...\nx86 host...\n...\n"
288
// Not machine-readable, but checking strings.Contains() should be fine.
289
CPUHelp []byte
290
}
291
292
func inspectFeatures(ctx context.Context, exe, machine string) (*features, error) {
293
var (
294
f features
295
stdout bytes.Buffer
296
stderr bytes.Buffer
297
)
298
cmd := exec.CommandContext(ctx, exe, "-M", "none", "-accel", "help")
299
cmd.Stdout = &stdout
300
cmd.Stderr = &stderr
301
if err := cmd.Run(); err != nil {
302
return nil, fmt.Errorf("failed to run %v: stdout=%q, stderr=%q", cmd.Args, stdout.String(), stderr.String())
303
}
304
f.AccelHelp = stdout.Bytes()
305
// on older versions qemu will write "help" output to stderr
306
if len(f.AccelHelp) == 0 {
307
f.AccelHelp = stderr.Bytes()
308
}
309
310
cmd = exec.CommandContext(ctx, exe, "-M", "none", "-netdev", "help")
311
cmd.Stdout = &stdout
312
cmd.Stderr = &stderr
313
if err := cmd.Run(); err != nil {
314
logrus.Warnf("failed to run %v: stdout=%q, stderr=%q", cmd.Args, stdout.String(), stderr.String())
315
} else {
316
f.NetdevHelp = stdout.Bytes()
317
if len(f.NetdevHelp) == 0 {
318
f.NetdevHelp = stderr.Bytes()
319
}
320
}
321
322
cmd = exec.CommandContext(ctx, exe, "-machine", "help")
323
cmd.Stdout = &stdout
324
cmd.Stderr = &stderr
325
if err := cmd.Run(); err != nil {
326
logrus.Warnf("failed to run %v: stdout=%q, stderr=%q", cmd.Args, stdout.String(), stderr.String())
327
} else {
328
f.MachineHelp = stdout.Bytes()
329
if len(f.MachineHelp) == 0 {
330
f.MachineHelp = stderr.Bytes()
331
}
332
}
333
334
// Avoid error: "No machine specified, and there is no default"
335
cmd = exec.CommandContext(ctx, exe, "-cpu", "help", "-machine", machine)
336
cmd.Stdout = &stdout
337
cmd.Stderr = &stderr
338
if err := cmd.Run(); err != nil {
339
logrus.Warnf("failed to run %v: stdout=%q, stderr=%q", cmd.Args, stdout.String(), stderr.String())
340
} else {
341
f.CPUHelp = stdout.Bytes()
342
if len(f.CPUHelp) == 0 {
343
f.CPUHelp = stderr.Bytes()
344
}
345
}
346
347
return &f, nil
348
}
349
350
// adjustMemBytesDarwinARM64HVF adjusts the memory to be <= 3 GiB, only when the following conditions are met:
351
//
352
// - Host OS < macOS 12.4
353
// - Host Arch == arm64
354
// - Accel == hvf
355
//
356
// This adjustment is required for avoiding host kernel panic. The issue was fixed in macOS 12.4 Beta 1.
357
// See https://github.com/lima-vm/lima/issues/795 https://gitlab.com/qemu-project/qemu/-/issues/903#note_911000975
358
func adjustMemBytesDarwinARM64HVF(memBytes int64, accel string) int64 {
359
const safeSize = 3 * 1024 * 1024 * 1024 // 3 GiB
360
if memBytes <= safeSize {
361
return memBytes
362
}
363
if runtime.GOOS != "darwin" {
364
return memBytes
365
}
366
if runtime.GOARCH != "arm64" {
367
return memBytes
368
}
369
if accel != "hvf" {
370
return memBytes
371
}
372
macOSProductVersion, err := osutil.ProductVersion()
373
if err != nil {
374
logrus.Warn(err)
375
return memBytes
376
}
377
if !macOSProductVersion.LessThan(*semver.New("12.4.0")) {
378
return memBytes
379
}
380
logrus.Warnf("Reducing the guest memory from %s to %s, to avoid host kernel panic on macOS <= 12.3; "+
381
"Please update macOS to 12.4 or later; "+
382
"See https://github.com/lima-vm/lima/issues/795 for the further background.",
383
units.BytesSize(float64(memBytes)), units.BytesSize(float64(safeSize)))
384
memBytes = safeSize
385
return memBytes
386
}
387
388
// qemuMachine returns string to use for -machine.
389
func qemuMachine(arch limatype.Arch) string {
390
if arch == limatype.X8664 {
391
return "q35"
392
}
393
return "virt"
394
}
395
396
// audioDevice returns the default audio device.
397
func audioDevice() string {
398
switch runtime.GOOS {
399
case "darwin":
400
return "coreaudio"
401
case "linux":
402
return "pa" // pulseaudio
403
case "windows":
404
return "dsound"
405
}
406
return "oss"
407
}
408
409
func defaultCPUType() limatype.CPUType {
410
// x86_64 + TCG + max was previously unstable until 2021.
411
// https://bugzilla.redhat.com/show_bug.cgi?id=1999700
412
// https://bugs.launchpad.net/qemu/+bug/1748296
413
defaultX8664 := "max"
414
if runtime.GOOS == "windows" && runtime.GOARCH == "amd64" {
415
// https://github.com/lima-vm/lima/pull/3487#issuecomment-2846253560
416
// > #931 intentionally prevented the code from setting it to max when running on Windows,
417
// > and kept it at qemu64.
418
//
419
// TODO: remove this if "max" works with the latest qemu
420
defaultX8664 = "qemu64"
421
}
422
cpuType := map[limatype.Arch]string{
423
limatype.AARCH64: "max",
424
limatype.ARMV7L: "max",
425
limatype.X8664: defaultX8664,
426
limatype.PPC64LE: "max",
427
limatype.RISCV64: "max",
428
limatype.S390X: "max",
429
}
430
for arch := range cpuType {
431
if limayaml.IsNativeArch(arch) && Accel(arch) != "tcg" {
432
if hasHostCPU() {
433
cpuType[arch] = "host"
434
}
435
}
436
if arch == limatype.X8664 && runtime.GOOS == "darwin" {
437
// disable AVX-512, since it requires trapping instruction faults in guest
438
// Enterprise Linux requires either v2 (SSE4) or v3 (AVX2), but not yet v4.
439
cpuType[arch] += ",-avx512vl"
440
441
// Disable pdpe1gb on Intel Mac
442
// https://github.com/lima-vm/lima/issues/1485
443
// https://stackoverflow.com/a/72863744/5167443
444
cpuType[arch] += ",-pdpe1gb"
445
}
446
}
447
return cpuType
448
}
449
450
func resolveCPUType(y *limatype.LimaYAML) string {
451
cpuType := defaultCPUType()
452
var overrideCPUType bool
453
var qemuOpts limatype.QEMUOpts
454
if err := limayaml.Convert(y.VMOpts[limatype.QEMU], &qemuOpts, "vmOpts.qemu"); err != nil {
455
logrus.WithError(err).Warnf("Couldn't convert %q", y.VMOpts[limatype.QEMU])
456
}
457
for k, v := range qemuOpts.CPUType {
458
if !slices.Contains(limatype.ArchTypes, *y.Arch) {
459
logrus.Warnf("field `vmOpts.qemu.cpuType` uses unsupported arch %q", k)
460
continue
461
}
462
if v != "" {
463
overrideCPUType = true
464
cpuType[k] = v
465
}
466
}
467
if overrideCPUType {
468
qemuOpts.CPUType = cpuType
469
if y.VMOpts == nil {
470
y.VMOpts = limatype.VMOpts{}
471
}
472
y.VMOpts[limatype.QEMU] = qemuOpts
473
}
474
475
return cpuType[*y.Arch]
476
}
477
478
func Cmdline(ctx context.Context, cfg Config) (exe string, args []string, err error) {
479
y := cfg.LimaYAML
480
exe, args, err = Exe(*y.Arch)
481
if err != nil {
482
return "", nil, err
483
}
484
485
features, err := inspectFeatures(ctx, exe, qemuMachine(*y.Arch))
486
if err != nil {
487
return "", nil, err
488
}
489
490
version, err := getQemuVersion(ctx, exe)
491
if err != nil {
492
logrus.WithError(err).Warning("Failed to detect QEMU version")
493
} else {
494
logrus.Debugf("QEMU version %s detected", version.String())
495
hardMin, softMin := minimumQemuVersion()
496
if version.LessThan(hardMin) {
497
logrus.Fatalf("QEMU %v is too old, %v or later required", version, hardMin)
498
}
499
if version.LessThan(softMin) {
500
logrus.Warnf("QEMU %v is too old, %v or later is recommended", version, softMin)
501
}
502
var qemuOpts limatype.QEMUOpts
503
if err := limayaml.Convert(y.VMOpts[limatype.QEMU], &qemuOpts, "vmOpts.qemu"); err != nil {
504
logrus.WithError(err).Warnf("Couldn't convert %q", y.VMOpts[limatype.QEMU])
505
}
506
if qemuOpts.MinimumVersion != nil && version.LessThan(*semver.New(*qemuOpts.MinimumVersion)) {
507
logrus.Fatalf("QEMU %v is too old, template requires %q or later", version, *qemuOpts.MinimumVersion)
508
}
509
}
510
511
// Architecture
512
accel := Accel(*y.Arch)
513
if !strings.Contains(string(features.AccelHelp), accel) {
514
return "", nil, fmt.Errorf("accelerator %q is not supported by %s", accel, exe)
515
}
516
517
// Memory
518
memBytes, err := units.RAMInBytes(*y.Memory)
519
if err != nil {
520
return "", nil, err
521
}
522
memBytes = adjustMemBytesDarwinARM64HVF(memBytes, accel)
523
args = appendArgsIfNoConflict(args, "-m", strconv.Itoa(int(memBytes>>20)))
524
525
if *y.MountType == limatype.VIRTIOFS {
526
args = appendArgsIfNoConflict(args, "-object",
527
fmt.Sprintf("memory-backend-file,id=virtiofs-shm,size=%s,mem-path=/dev/shm,share=on", strconv.Itoa(int(memBytes))))
528
args = appendArgsIfNoConflict(args, "-numa", "node,memdev=virtiofs-shm")
529
}
530
531
// CPU
532
cpu := resolveCPUType(y)
533
if runtime.GOOS == "darwin" && runtime.GOARCH == "amd64" {
534
switch {
535
case strings.HasPrefix(cpu, "host"), strings.HasPrefix(cpu, "max"):
536
if !strings.Contains(cpu, ",-pdpe1gb") {
537
logrus.Warnf("On Intel Mac, CPU type %q typically needs \",-pdpe1gb\" option (https://stackoverflow.com/a/72863744/5167443)", cpu)
538
}
539
}
540
}
541
// `qemu-system-ppc64 -help` does not show "max", but it is actually accepted
542
if cpu != "max" && !strings.Contains(string(features.CPUHelp), strings.Split(cpu, ",")[0]) {
543
return "", nil, fmt.Errorf("cpu %q is not supported by %s", cpu, exe)
544
}
545
args = appendArgsIfNoConflict(args, "-cpu", cpu)
546
547
// Machine
548
switch *y.Arch {
549
case limatype.X8664:
550
switch accel {
551
case "tcg":
552
// use q35 machine with vmware io port disabled.
553
args = appendArgsIfNoConflict(args, "-machine", "q35,vmport=off")
554
// use tcg accelerator with multi threading with 512MB translation block size
555
// https://qemu-project.gitlab.io/qemu/devel/multi-thread-tcg.html?highlight=tcg
556
// https://qemu-project.gitlab.io/qemu/system/invocation.html?highlight=tcg%20opts
557
// this will make sure each vCPU will be backed by 1 host user thread.
558
args = appendArgsIfNoConflict(args, "-accel", "tcg,thread=multi,tb-size=512")
559
// This will disable CPU S3/S4 state.
560
args = append(args, "-global", "ICH9-LPC.disable_s3=1")
561
args = append(args, "-global", "ICH9-LPC.disable_s4=1")
562
case "whpx":
563
// whpx: injection failed, MSI (0, 0) delivery: 0, dest_mode: 0, trigger mode: 0, vector: 0
564
args = appendArgsIfNoConflict(args, "-machine", "q35,accel="+accel+",kernel-irqchip=off")
565
default:
566
args = appendArgsIfNoConflict(args, "-machine", "q35,accel="+accel)
567
}
568
case limatype.AARCH64:
569
machine := "virt,accel=" + accel
570
args = appendArgsIfNoConflict(args, "-machine", machine)
571
case limatype.RISCV64:
572
// https://github.com/tianocore/edk2/blob/edk2-stable202408/OvmfPkg/RiscVVirt/README.md#test
573
// > Note: the `acpi=off` machine property is specified because Linux guest
574
// > support for ACPI (that is, the ACPI consumer side) is a work in progress.
575
// > Currently, `acpi=off` is recommended unless you are developing ACPI support
576
// > yourself.
577
machine := "virt,acpi=off,accel=" + accel
578
args = appendArgsIfNoConflict(args, "-machine", machine)
579
case limatype.ARMV7L:
580
machine := "virt,accel=" + accel
581
args = appendArgsIfNoConflict(args, "-machine", machine)
582
case limatype.PPC64LE:
583
machine := "pseries,accel=" + accel
584
args = appendArgsIfNoConflict(args, "-machine", machine)
585
case limatype.S390X:
586
machine := "s390-ccw-virtio,accel=" + accel
587
args = appendArgsIfNoConflict(args, "-machine", machine)
588
}
589
590
// SMP
591
args = appendArgsIfNoConflict(args, "-smp",
592
fmt.Sprintf("%d,sockets=1,cores=%d,threads=1", *y.CPUs, *y.CPUs))
593
594
// Firmware
595
legacyBIOS := *y.Firmware.LegacyBIOS
596
if legacyBIOS && *y.Arch != limatype.X8664 && *y.Arch != limatype.ARMV7L {
597
logrus.Warnf("field `firmware.legacyBIOS` is not supported for architecture %q, ignoring", *y.Arch)
598
legacyBIOS = false
599
}
600
noFirmware := *y.Arch == limatype.PPC64LE || *y.Arch == limatype.S390X || legacyBIOS
601
if !noFirmware {
602
var firmware string
603
firmwareInBios := runtime.GOOS == "windows"
604
if envVar := os.Getenv("_LIMA_QEMU_UEFI_IN_BIOS"); envVar != "" {
605
b, err := strconv.ParseBool(envVar)
606
if err != nil {
607
logrus.WithError(err).Warnf("invalid _LIMA_QEMU_UEFI_IN_BIOS value %q", envVar)
608
} else {
609
firmwareInBios = b
610
}
611
}
612
firmwareInBios = firmwareInBios && *y.Arch == limatype.X8664
613
downloadedFirmware := filepath.Join(cfg.InstanceDir, filenames.QemuEfiCodeFD)
614
firmwareWithVars := filepath.Join(cfg.InstanceDir, filenames.QemuEfiFullFD)
615
if firmwareInBios {
616
if _, stErr := os.Stat(firmwareWithVars); stErr == nil {
617
firmware = firmwareWithVars
618
logrus.Infof("Using existing firmware (%q)", firmware)
619
}
620
} else {
621
if _, stErr := os.Stat(downloadedFirmware); errors.Is(stErr, os.ErrNotExist) {
622
loop:
623
for _, f := range y.Firmware.Images {
624
switch f.VMType {
625
case "", limatype.QEMU:
626
if f.Arch == *y.Arch {
627
if _, err = fileutils.DownloadFile(ctx, downloadedFirmware, f.File, true, "UEFI code "+f.Location, *y.Arch); err != nil {
628
logrus.WithError(err).Warnf("failed to download %q", f.Location)
629
continue loop
630
}
631
firmware = downloadedFirmware
632
logrus.Infof("Using firmware %q (downloaded from %q)", firmware, f.Location)
633
break loop
634
}
635
}
636
}
637
} else {
638
firmware = downloadedFirmware
639
logrus.Infof("Using existing firmware (%q)", firmware)
640
}
641
}
642
if firmware == "" {
643
firmware, err = getFirmware(exe, *y.Arch)
644
if err != nil {
645
return "", nil, err
646
}
647
logrus.Infof("Using system firmware (%q)", firmware)
648
if firmwareInBios {
649
firmwareVars, err := getFirmwareVars(exe, *y.Arch)
650
if err != nil {
651
return "", nil, err
652
}
653
logrus.Infof("Using system firmware vars (%q)", firmwareVars)
654
varsFile, err := os.Open(firmwareVars)
655
if err != nil {
656
return "", nil, err
657
}
658
defer varsFile.Close()
659
codeFile, err := os.Open(firmware)
660
if err != nil {
661
return "", nil, err
662
}
663
defer codeFile.Close()
664
resultFile, err := os.OpenFile(firmwareWithVars, os.O_CREATE|os.O_WRONLY, 0o644)
665
if err != nil {
666
return "", nil, err
667
}
668
defer resultFile.Close()
669
_, err = io.Copy(resultFile, varsFile)
670
if err != nil {
671
return "", nil, err
672
}
673
_, err = io.Copy(resultFile, codeFile)
674
if err != nil {
675
return "", nil, err
676
}
677
firmware = firmwareWithVars
678
}
679
}
680
if firmware != "" {
681
if firmwareInBios {
682
args = append(args, "-bios", firmware)
683
} else {
684
args = append(args, "-drive", fmt.Sprintf("if=pflash,format=raw,readonly=on,file=%s", firmware))
685
}
686
}
687
}
688
689
// Disk
690
baseDisk := filepath.Join(cfg.InstanceDir, filenames.BaseDisk)
691
diffDisk := filepath.Join(cfg.InstanceDir, filenames.DiffDisk)
692
extraDisks := []string{}
693
for _, d := range y.AdditionalDisks {
694
diskName := d.Name
695
disk, err := store.InspectDisk(diskName, d.FSType)
696
if err != nil {
697
logrus.Errorf("could not load disk %q: %q", diskName, err)
698
return "", nil, err
699
}
700
701
if disk.Instance != "" {
702
if disk.InstanceDir != cfg.InstanceDir {
703
logrus.Errorf("could not attach disk %q, in use by instance %q", diskName, disk.Instance)
704
return "", nil, err
705
}
706
err = disk.Unlock()
707
if err != nil {
708
logrus.Errorf("could not unlock disk %q to reuse in the same instance %q", diskName, cfg.Name)
709
return "", nil, err
710
}
711
}
712
logrus.Infof("Mounting disk %q on %q", diskName, disk.MountPoint)
713
err = disk.Lock(cfg.InstanceDir)
714
if err != nil {
715
logrus.Errorf("could not lock disk %q: %q", diskName, err)
716
return "", nil, err
717
}
718
dataDisk := filepath.Join(disk.Dir, filenames.DataDisk)
719
extraDisks = append(extraDisks, dataDisk)
720
}
721
722
isBaseDiskCDROM, err := iso9660util.IsISO9660(baseDisk)
723
if err != nil {
724
return "", nil, err
725
}
726
if isBaseDiskCDROM {
727
args = appendArgsIfNoConflict(args, "-boot", "order=d,splash-time=0,menu=on")
728
args = append(args, "-drive", fmt.Sprintf("file=%s,format=raw,media=cdrom,readonly=on", baseDisk))
729
} else {
730
args = appendArgsIfNoConflict(args, "-boot", "order=c,splash-time=0,menu=on")
731
}
732
if diskSize, _ := units.RAMInBytes(*cfg.LimaYAML.Disk); diskSize > 0 {
733
args = append(args, "-drive", fmt.Sprintf("file=%s,if=virtio,discard=on", diffDisk))
734
} else if !isBaseDiskCDROM {
735
baseDiskInfo, err := qemuimgutil.GetInfo(ctx, baseDisk)
736
if err != nil {
737
return "", nil, fmt.Errorf("failed to get the information of %q: %w", baseDisk, err)
738
}
739
if err = qemuimgutil.AcceptableAsBaseDisk(baseDiskInfo); err != nil {
740
return "", nil, fmt.Errorf("file %q is not acceptable as the base disk: %w", baseDisk, err)
741
}
742
if baseDiskInfo.Format == "" {
743
return "", nil, fmt.Errorf("failed to inspect the format of %q", baseDisk)
744
}
745
args = append(args, "-drive", fmt.Sprintf("file=%s,format=%s,if=virtio,discard=on", baseDisk, baseDiskInfo.Format))
746
}
747
for _, extraDisk := range extraDisks {
748
args = append(args, "-drive", fmt.Sprintf("file=%s,if=virtio,discard=on", extraDisk))
749
}
750
751
// cloud-init
752
args = append(args,
753
"-drive", "id=cdrom0,if=none,format=raw,readonly=on,file="+filepath.Join(cfg.InstanceDir, filenames.CIDataISO),
754
"-device", "virtio-scsi,id=scsi0",
755
"-device", "scsi-cd,bus=scsi0.0,drive=cdrom0")
756
757
// Kernel
758
kernel := filepath.Join(cfg.InstanceDir, filenames.Kernel)
759
kernelCmdline := filepath.Join(cfg.InstanceDir, filenames.KernelCmdline)
760
initrd := filepath.Join(cfg.InstanceDir, filenames.Initrd)
761
if _, err := os.Stat(kernel); err == nil {
762
args = appendArgsIfNoConflict(args, "-kernel", kernel)
763
}
764
if b, err := os.ReadFile(kernelCmdline); err == nil {
765
args = appendArgsIfNoConflict(args, "-append", string(b))
766
}
767
if _, err := os.Stat(initrd); err == nil {
768
args = appendArgsIfNoConflict(args, "-initrd", initrd)
769
}
770
771
// Network
772
// Configure default usernetwork with limayaml.MACAddress(driver.Instance.Dir) for eth0 interface
773
firstUsernetIndex := limayaml.FirstUsernetIndex(y)
774
if firstUsernetIndex == -1 {
775
args = append(args, "-netdev", fmt.Sprintf("user,id=net0,net=%s,dhcpstart=%s,hostfwd=tcp:%s:%d-:22",
776
networks.SlirpNetwork, networks.SlirpIPAddress, cfg.SSHAddress, cfg.SSHLocalPort))
777
} else {
778
qemuSock, err := usernet.Sock(y.Networks[firstUsernetIndex].Lima, usernet.QEMUSock)
779
if err != nil {
780
return "", nil, err
781
}
782
args = append(args, "-netdev", fmt.Sprintf("socket,id=net0,fd={{ fd_connect %q }}", qemuSock))
783
}
784
virtioNet := "virtio-net-pci"
785
if *y.Arch == limatype.S390X {
786
// virtio-net-pci does not work on EL, while it works on Ubuntu
787
// https://github.com/lima-vm/lima/pull/3319/files#r1986388345
788
virtioNet = "virtio-net-ccw"
789
}
790
args = append(args, "-device", virtioNet+",netdev=net0,mac="+limayaml.MACAddress(cfg.InstanceDir))
791
792
for i, nw := range y.Networks {
793
if nw.Lima != "" {
794
nwCfg, err := networks.LoadConfig()
795
if err != nil {
796
return "", nil, err
797
}
798
799
// Handle usernet connections
800
isUsernet, err := nwCfg.Usernet(nw.Lima)
801
if err != nil {
802
return "", nil, err
803
}
804
if isUsernet {
805
if i == firstUsernetIndex {
806
continue
807
}
808
qemuSock, err := usernet.Sock(nw.Lima, usernet.QEMUSock)
809
if err != nil {
810
return "", nil, err
811
}
812
args = append(args, "-netdev", fmt.Sprintf("socket,id=net%d,fd={{ fd_connect %q }}", i+1, qemuSock))
813
args = append(args, "-device", fmt.Sprintf("%s,netdev=net%d,mac=%s", virtioNet, i+1, nw.MACAddress))
814
} else {
815
if runtime.GOOS != "darwin" {
816
return "", nil, fmt.Errorf("networks.yaml '%s' configuration is only supported on macOS right now", nw.Lima)
817
}
818
logrus.Debugf("Using socketVMNet (%q)", nwCfg.Paths.SocketVMNet)
819
sock, err := networks.Sock(nw.Lima)
820
if err != nil {
821
return "", nil, err
822
}
823
args = append(args, "-netdev", fmt.Sprintf("socket,id=net%d,fd={{ fd_connect %q }}", i+1, sock))
824
// TODO: should we also validate that the socket exists, or do we rely on the
825
// networks reconciler to throw an error when the network cannot start?
826
}
827
} else if nw.Socket != "" {
828
args = append(args, "-netdev", fmt.Sprintf("socket,id=net%d,fd={{ fd_connect %q }}", i+1, nw.Socket))
829
} else {
830
return "", nil, fmt.Errorf("invalid network spec %+v", nw)
831
}
832
args = append(args, "-device", fmt.Sprintf("%s,netdev=net%d,mac=%s", virtioNet, i+1, nw.MACAddress))
833
}
834
835
// virtio-rng-pci accelerates starting up the OS, according to https://wiki.gentoo.org/wiki/QEMU/Options
836
args = append(args, "-device", "virtio-rng-pci")
837
838
// Input
839
input := "mouse"
840
841
// Sound
842
if *y.Audio.Device != "" {
843
id := "default"
844
// audio device
845
audiodev := *y.Audio.Device
846
if audiodev == "default" {
847
audiodev = audioDevice()
848
}
849
audiodev += fmt.Sprintf(",id=%s", id)
850
args = append(args, "-audiodev", audiodev)
851
// audio controller
852
args = append(args, "-device", "ich9-intel-hda")
853
// audio codec
854
args = append(args, "-device", fmt.Sprintf("hda-output,audiodev=%s", id))
855
}
856
// Graphics
857
if *y.Video.Display != "" {
858
display := *y.Video.Display
859
if display == "vnc" {
860
display += "=" + *y.Video.VNC.Display
861
display += ",password=on"
862
// use tablet to avoid double cursors
863
input = "tablet"
864
}
865
args = appendArgsIfNoConflict(args, "-display", display)
866
}
867
868
if *y.Video.Display != "none" {
869
switch *y.Arch {
870
// FIXME: use virtio-gpu on all the architectures
871
case limatype.X8664, limatype.RISCV64:
872
args = append(args, "-device", "virtio-vga")
873
default:
874
args = append(args, "-device", "virtio-gpu")
875
}
876
args = append(args, "-device", "virtio-keyboard-pci")
877
args = append(args, "-device", "virtio-"+input+"-pci")
878
args = append(args, "-device", "qemu-xhci,id=usb-bus")
879
}
880
881
// Parallel
882
args = append(args, "-parallel", "none")
883
884
// Serial (default)
885
// This is ttyS0 for Intel and RISC-V, ttyAMA0 for ARM.
886
serialSock := filepath.Join(cfg.InstanceDir, filenames.SerialSock)
887
if err := os.RemoveAll(serialSock); err != nil {
888
return "", nil, err
889
}
890
serialLog := filepath.Join(cfg.InstanceDir, filenames.SerialLog)
891
if err := os.RemoveAll(serialLog); err != nil {
892
return "", nil, err
893
}
894
const serialChardev = "char-serial"
895
args = append(args, "-chardev", fmt.Sprintf("socket,id=%s,path=%s,server=on,wait=off,logfile=%s", serialChardev, serialSock, serialLog))
896
args = append(args, "-serial", "chardev:"+serialChardev)
897
898
// Serial (PCI, ARM only)
899
// On ARM, the default serial is ttyAMA0, this PCI serial is ttyS0.
900
// https://gitlab.com/qemu-project/qemu/-/issues/1801#note_1494720586
901
switch *y.Arch {
902
case limatype.AARCH64, limatype.ARMV7L:
903
serialpSock := filepath.Join(cfg.InstanceDir, filenames.SerialPCISock)
904
if err := os.RemoveAll(serialpSock); err != nil {
905
return "", nil, err
906
}
907
serialpLog := filepath.Join(cfg.InstanceDir, filenames.SerialPCILog)
908
if err := os.RemoveAll(serialpLog); err != nil {
909
return "", nil, err
910
}
911
const serialpChardev = "char-serial-pci"
912
args = append(args, "-chardev", fmt.Sprintf("socket,id=%s,path=%s,server=on,wait=off,logfile=%s", serialpChardev, serialpSock, serialpLog))
913
args = append(args, "-device", "pci-serial,chardev="+serialpChardev)
914
}
915
916
// Serial (virtio)
917
serialvSock := filepath.Join(cfg.InstanceDir, filenames.SerialVirtioSock)
918
if err := os.RemoveAll(serialvSock); err != nil {
919
return "", nil, err
920
}
921
serialvLog := filepath.Join(cfg.InstanceDir, filenames.SerialVirtioLog)
922
if err := os.RemoveAll(serialvLog); err != nil {
923
return "", nil, err
924
}
925
const serialvChardev = "char-serial-virtio"
926
args = append(args, "-chardev", fmt.Sprintf("socket,id=%s,path=%s,server=on,wait=off,logfile=%s", serialvChardev, serialvSock, serialvLog))
927
// max_ports=1 is required for https://github.com/lima-vm/lima/issues/1689 https://github.com/lima-vm/lima/issues/1691
928
serialvMaxPorts := 1
929
if *y.Arch == limatype.S390X {
930
serialvMaxPorts++ // needed to avoid `virtio-serial-bus: Out-of-range port id specified, max. allowed: 0`
931
}
932
args = append(args, "-device", fmt.Sprintf("virtio-serial-pci,id=virtio-serial0,max_ports=%d", serialvMaxPorts))
933
args = append(args, "-device", fmt.Sprintf("virtconsole,chardev=%s,id=console0", serialvChardev))
934
935
// We also want to enable vsock here, but QEMU does not support vsock for macOS hosts
936
937
if *y.MountType == limatype.NINEP || *y.MountType == limatype.VIRTIOFS {
938
for i, f := range y.Mounts {
939
tag := limayaml.MountTag(f.Location, *f.MountPoint)
940
if err := os.MkdirAll(f.Location, 0o755); err != nil {
941
return "", nil, err
942
}
943
944
switch *y.MountType {
945
case limatype.NINEP:
946
options := "local"
947
options += fmt.Sprintf(",mount_tag=%s", tag)
948
options += fmt.Sprintf(",path=%s", f.Location)
949
options += fmt.Sprintf(",security_model=%s", *f.NineP.SecurityModel)
950
if !*f.Writable {
951
options += ",readonly=on"
952
}
953
args = append(args, "-virtfs", options)
954
case limatype.VIRTIOFS:
955
// Note that read-only mode is not supported on the QEMU/virtiofsd side yet:
956
// https://gitlab.com/virtio-fs/virtiofsd/-/issues/97
957
chardev := fmt.Sprintf("char-virtiofs-%d", i)
958
vhostSock := filepath.Join(cfg.InstanceDir, fmt.Sprintf(filenames.VhostSock, i))
959
args = append(args, "-chardev", fmt.Sprintf("socket,id=%s,path=%s", chardev, vhostSock))
960
961
options := "vhost-user-fs-pci"
962
options += fmt.Sprintf(",queue-size=%d", *f.Virtiofs.QueueSize)
963
options += fmt.Sprintf(",chardev=%s", chardev)
964
options += fmt.Sprintf(",tag=%s", tag)
965
args = append(args, "-device", options)
966
}
967
}
968
}
969
970
// QMP
971
qmpSock := filepath.Join(cfg.InstanceDir, filenames.QMPSock)
972
if err := os.RemoveAll(qmpSock); err != nil {
973
return "", nil, err
974
}
975
const qmpChardev = "char-qmp"
976
args = append(args, "-chardev", fmt.Sprintf("socket,id=%s,path=%s,server=on,wait=off", qmpChardev, qmpSock))
977
args = append(args, "-qmp", "chardev:"+qmpChardev)
978
979
if cfg.VirtioGA {
980
// Guest agent via serialport
981
guestSock := filepath.Join(cfg.InstanceDir, filenames.GuestAgentSock)
982
args = append(args, "-chardev", fmt.Sprintf("socket,path=%s,server=on,wait=off,id=qga0", guestSock))
983
args = append(args, "-device", "virtio-serial")
984
args = append(args, "-device", "virtserialport,chardev=qga0,name="+filenames.VirtioPort)
985
}
986
987
// QEMU process
988
args = append(args, "-name", "lima-"+cfg.Name)
989
args = append(args, "-pidfile", filepath.Join(cfg.InstanceDir, filenames.PIDFile(*y.VMType)))
990
991
return exe, args, nil
992
}
993
994
func FindVirtiofsd(ctx context.Context, qemuExe string) (string, error) {
995
type vhostUserBackend struct {
996
BackendType string `json:"type"`
997
Binary string `json:"binary"`
998
}
999
1000
homeDir, err := os.UserHomeDir()
1001
if err != nil {
1002
return "", err
1003
}
1004
1005
const relativePath = "share/qemu/vhost-user"
1006
1007
binDir := filepath.Dir(qemuExe) // "/usr/local/bin"
1008
usrDir := filepath.Dir(binDir) // "/usr/local"
1009
userLocalDir := filepath.Join(homeDir, ".local") // "$HOME/.local"
1010
1011
candidates := []string{
1012
filepath.Join(userLocalDir, relativePath),
1013
filepath.Join(usrDir, relativePath),
1014
}
1015
1016
if usrDir != "/usr" {
1017
candidates = append(candidates, filepath.Join("/usr", relativePath))
1018
}
1019
1020
for _, vhostCfgsDir := range candidates {
1021
logrus.Debugf("Checking vhost directory %s", vhostCfgsDir)
1022
1023
cfgEntries, err := os.ReadDir(vhostCfgsDir)
1024
if err != nil {
1025
logrus.Debugf("Failed to list vhost directory: %v", err)
1026
continue
1027
}
1028
1029
for _, cfgEntry := range cfgEntries {
1030
logrus.Debugf("Checking vhost vhostCfg %s", cfgEntry.Name())
1031
if !strings.HasSuffix(cfgEntry.Name(), ".json") {
1032
continue
1033
}
1034
1035
var vhostCfg vhostUserBackend
1036
contents, err := os.ReadFile(filepath.Join(vhostCfgsDir, cfgEntry.Name()))
1037
if err == nil {
1038
err = json.Unmarshal(contents, &vhostCfg)
1039
}
1040
1041
if err != nil {
1042
logrus.Warnf("Failed to load vhost-user config %s: %v", cfgEntry.Name(), err)
1043
continue
1044
}
1045
logrus.Debugf("%v", vhostCfg)
1046
1047
if vhostCfg.BackendType != "fs" {
1048
continue
1049
}
1050
1051
// Only rust virtiofsd supports --version, so use that to make sure this isn't
1052
// QEMU's virtiofsd, which requires running as root.
1053
cmd := exec.CommandContext(ctx, vhostCfg.Binary, "--version")
1054
output, err := cmd.CombinedOutput()
1055
if err != nil {
1056
logrus.Warnf("Failed to run %s --version (is this QEMU virtiofsd?): %s: %s",
1057
vhostCfg.Binary, err, output)
1058
continue
1059
}
1060
1061
return vhostCfg.Binary, nil
1062
}
1063
}
1064
1065
return "", errors.New("failed to locate virtiofsd")
1066
}
1067
1068
func VirtiofsdCmdline(cfg Config, mountIndex int) ([]string, error) {
1069
mount := cfg.LimaYAML.Mounts[mountIndex]
1070
1071
vhostSock := filepath.Join(cfg.InstanceDir, fmt.Sprintf(filenames.VhostSock, mountIndex))
1072
// qemu_driver has to wait for the socket to appear, so make sure any old ones are removed here.
1073
if err := os.Remove(vhostSock); err != nil && !errors.Is(err, fs.ErrNotExist) {
1074
logrus.Warnf("Failed to remove old vhost socket: %v", err)
1075
}
1076
1077
return []string{
1078
"--socket-path", vhostSock,
1079
"--shared-dir", mount.Location,
1080
}, nil
1081
}
1082
1083
// qemuArch returns the arch string used by qemu.
1084
func qemuArch(arch limatype.Arch) string {
1085
switch arch {
1086
case limatype.ARMV7L:
1087
return "arm"
1088
case limatype.PPC64LE:
1089
return "ppc64"
1090
default:
1091
return arch
1092
}
1093
}
1094
1095
// qemuEdk2 returns the arch string used by `/usr/local/share/qemu/edk2-*-code.fd`.
1096
func qemuEdk2Arch(arch limatype.Arch) string {
1097
if arch == limatype.RISCV64 {
1098
return "riscv"
1099
}
1100
return qemuArch(arch)
1101
}
1102
1103
func Exe(arch limatype.Arch) (exe string, args []string, err error) {
1104
exeBase := "qemu-system-" + qemuArch(arch)
1105
envK := "QEMU_SYSTEM_" + strings.ToUpper(qemuArch(arch))
1106
if envV := os.Getenv(envK); envV != "" {
1107
ss, err := shellwords.Parse(envV)
1108
if err != nil {
1109
return "", nil, fmt.Errorf("failed to parse %s value %q: %w", envK, envV, err)
1110
}
1111
exeBase, args = ss[0], ss[1:]
1112
if len(args) != 0 {
1113
logrus.Warnf("Specifying args (%v) via $%s is supported only for debugging!", args, envK)
1114
}
1115
}
1116
exe, err = exec.LookPath(exeBase)
1117
if err != nil {
1118
return "", nil, err
1119
}
1120
return exe, args, nil
1121
}
1122
1123
func Accel(arch limatype.Arch) string {
1124
if limayaml.IsNativeArch(arch) {
1125
switch runtime.GOOS {
1126
case "darwin":
1127
// TODO: return "tcg" if HVF is not available
1128
return "hvf"
1129
case "linux":
1130
if _, err := os.Stat("/dev/kvm"); err != nil {
1131
logrus.WithError(err).Warn("/dev/kvm is not available. Disabling KVM. Expect very poor performance.")
1132
return "tcg"
1133
}
1134
return "kvm"
1135
case "netbsd":
1136
return "nvmm"
1137
case "dragonfly":
1138
return "nvmm"
1139
case "windows":
1140
return "whpx"
1141
}
1142
}
1143
return "tcg"
1144
}
1145
1146
func parseQemuVersion(output string) (*semver.Version, error) {
1147
lines := strings.Split(output, "\n")
1148
regex := regexp.MustCompile(`^QEMU emulator version (\d+\.\d+\.\d+)`)
1149
matches := regex.FindStringSubmatch(lines[0])
1150
if len(matches) == 2 {
1151
return semver.New(matches[1]), nil
1152
}
1153
return &semver.Version{}, fmt.Errorf("failed to parse %v", output)
1154
}
1155
1156
func getQemuVersion(ctx context.Context, qemuExe string) (*semver.Version, error) {
1157
var (
1158
stdout bytes.Buffer
1159
stderr bytes.Buffer
1160
)
1161
cmd := exec.CommandContext(ctx, qemuExe, "--version")
1162
cmd.Stdout = &stdout
1163
cmd.Stderr = &stderr
1164
if err := cmd.Run(); err != nil {
1165
return nil, fmt.Errorf("failed to run %v: stdout=%q, stderr=%q", cmd.Args, stdout.String(), stderr.String())
1166
}
1167
1168
return parseQemuVersion(stdout.String())
1169
}
1170
1171
func getFirmware(qemuExe string, arch limatype.Arch) (string, error) {
1172
switch arch {
1173
case limatype.X8664, limatype.AARCH64, limatype.ARMV7L, limatype.RISCV64:
1174
default:
1175
return "", fmt.Errorf("unexpected architecture: %q", arch)
1176
}
1177
1178
homeDir, err := os.UserHomeDir()
1179
if err != nil {
1180
return "", err
1181
}
1182
1183
binDir := filepath.Dir(qemuExe) // "/usr/local/bin"
1184
localDir := filepath.Dir(binDir) // "/usr/local"
1185
userLocalDir := filepath.Join(homeDir, ".local") // "$HOME/.local"
1186
1187
relativePath := fmt.Sprintf("share/qemu/edk2-%s-code.fd", qemuEdk2Arch(arch))
1188
relativePathWin := fmt.Sprintf("share/edk2-%s-code.fd", qemuEdk2Arch(arch))
1189
candidates := []string{
1190
filepath.Join(userLocalDir, relativePath), // XDG-like
1191
filepath.Join(localDir, relativePath), // macOS (homebrew)
1192
filepath.Join(binDir, relativePathWin), // Windows installer
1193
}
1194
1195
switch arch {
1196
case limatype.X8664:
1197
// Archlinux package "edk2-ovmf"
1198
// @see: https://archlinux.org/packages/extra/any/edk2-ovmf/files
1199
candidates = append(candidates, "/usr/share/edk2/x64/OVMF_CODE.4m.fd")
1200
// Debian package "ovmf"
1201
candidates = append(candidates, "/usr/share/OVMF/OVMF_CODE.fd")
1202
candidates = append(candidates, "/usr/share/OVMF/OVMF_CODE_4M.fd")
1203
// Fedora package "edk2-ovmf"
1204
candidates = append(candidates, "/usr/share/edk2/ovmf/OVMF_CODE.fd")
1205
// openSUSE package "qemu-ovmf-x86_64"
1206
candidates = append(candidates, "/usr/share/qemu/ovmf-x86_64.bin")
1207
case limatype.AARCH64:
1208
// Archlinux package "edk2-aarch64"
1209
// @see: https://archlinux.org/packages/extra/any/edk2-aarch64/files
1210
candidates = append(candidates, "/usr/share/edk2/aarch64/QEMU_CODE.fd")
1211
// Debian package "qemu-efi-aarch64"
1212
// Fedora package "edk2-aarch64"
1213
candidates = append(candidates, "/usr/share/AAVMF/AAVMF_CODE.fd")
1214
// Debian package "qemu-efi-aarch64" (unpadded, backwards compatibility)
1215
candidates = append(candidates, "/usr/share/qemu-efi-aarch64/QEMU_EFI.fd")
1216
case limatype.ARMV7L:
1217
// Archlinux package "edk2-arm"
1218
// @see: https://archlinux.org/packages/extra/any/edk2-arm/files
1219
candidates = append(candidates, "/usr/share/edk2/arm/QEMU_CODE.fd")
1220
// Debian package "qemu-efi-arm"
1221
// Fedora package "edk2-arm"
1222
candidates = append(candidates, "/usr/share/AAVMF/AAVMF32_CODE.fd")
1223
case limatype.RISCV64:
1224
// Debian package "qemu-efi-riscv64"
1225
candidates = append(candidates, "/usr/share/qemu-efi-riscv64/RISCV_VIRT_CODE.fd")
1226
// Fedora package "edk2-riscv64"
1227
candidates = append(candidates, "/usr/share/edk2/riscv/RISCV_VIRT_CODE.fd")
1228
}
1229
1230
logrus.Debugf("firmware candidates = %v", candidates)
1231
1232
for _, f := range candidates {
1233
if _, err := os.Stat(f); err == nil {
1234
return f, nil
1235
}
1236
}
1237
1238
if arch == limatype.X8664 {
1239
return "", fmt.Errorf("could not find firmware for %q (hint: try setting `firmware.legacyBIOS` to `true`)", arch)
1240
}
1241
qemuArch := strings.TrimPrefix(filepath.Base(qemuExe), "qemu-system-")
1242
return "", fmt.Errorf("could not find firmware for %q (hint: try copying the \"edk-%s-code.fd\" firmware to $HOME/.local/share/qemu/)", arch, qemuArch)
1243
}
1244
1245
func getFirmwareVars(qemuExe string, arch limatype.Arch) (string, error) {
1246
var targetArch string
1247
switch arch {
1248
case limatype.X8664:
1249
targetArch = "i386" // vars are unified between i386 and x86_64 and normally only former is bundled
1250
default:
1251
return "", fmt.Errorf("unexpected architecture: %q", arch)
1252
}
1253
1254
homeDir, err := os.UserHomeDir()
1255
if err != nil {
1256
return "", err
1257
}
1258
1259
binDir := filepath.Dir(qemuExe) // "/usr/local/bin"
1260
localDir := filepath.Dir(binDir) // "/usr/local"
1261
userLocalDir := filepath.Join(homeDir, ".local") // "$HOME/.local"
1262
1263
relativePath := fmt.Sprintf("share/qemu/edk2-%s-vars.fd", qemuEdk2Arch(targetArch))
1264
relativePathWin := fmt.Sprintf("share/edk2-%s-vars.fd", qemuEdk2Arch(targetArch))
1265
candidates := []string{
1266
filepath.Join(userLocalDir, relativePath), // XDG-like
1267
filepath.Join(localDir, relativePath), // macOS (homebrew)
1268
filepath.Join(binDir, relativePathWin), // Windows installer
1269
}
1270
1271
logrus.Debugf("firmware vars candidates = %v", candidates)
1272
1273
for _, f := range candidates {
1274
if _, err := os.Stat(f); err == nil {
1275
return f, nil
1276
}
1277
}
1278
1279
return "", fmt.Errorf("could not find firmware vars for %q", arch)
1280
}
1281
1282
var hasSMEDarwin = sync.OnceValue(func() bool {
1283
if runtime.GOOS != "darwin" || runtime.GOARCH != "arm64" {
1284
return false
1285
}
1286
// golang.org/x/sys/cpu does not support inspecting the availability of SME yet
1287
s, err := osutil.Sysctl(context.Background(), "hw.optional.arm.FEAT_SME")
1288
if err != nil {
1289
logrus.WithError(err).Debug("failed to check hw.optional.arm.FEAT_SME")
1290
}
1291
return s == "1"
1292
})
1293
1294
func hasHostCPU() bool {
1295
switch runtime.GOOS {
1296
case "darwin":
1297
if hasSMEDarwin() {
1298
// [2025-02-05]
1299
// SME is available since Apple M4 running macOS 15.2, but it was broken on macOS 15.2.
1300
// It has been fixed in 15.3.
1301
//
1302
// https://github.com/lima-vm/lima/issues/3032
1303
// https://gitlab.com/qemu-project/qemu/-/issues/2665
1304
// https://gitlab.com/qemu-project/qemu/-/issues/2721
1305
1306
// [2025-02-12]
1307
// SME got broken again after upgrading QEMU from 9.2.0 to 9.2.1 (Homebrew bottle).
1308
// Possibly this regression happened in some build process rather than in QEMU itself?
1309
// https://github.com/lima-vm/lima/issues/3226
1310
return false
1311
}
1312
return true
1313
case "linux":
1314
return true
1315
case "netbsd", "windows":
1316
return false
1317
}
1318
// Not reached
1319
return false
1320
}
1321
1322