Repository: lima-vm/lima
Pull request #827 (2022-04-28 09:24:53)

experimental support for riscv64
```
$ limactl start template://experimental/riscv64
...
INFO[0201] READY. Run `limactl shell riscv64` to open the shell.  

$ limactl shell riscv64 uname -a
Linux lima-riscv64 5.15.0-1007-generic #7-Ubuntu SMP Tue Apr 12 16:07:30 UTC 2022 riscv64 riscv64 riscv64 GNU/Linux

$ limactl shell riscv64 cat /proc/cpuinfo
processor       : 0
hart            : 0
isa             : rv64imafdcsu
mmu             : sv48

processor       : 1
hart            : 1
isa             : rv64imafdcsu
mmu             : sv48

processor       : 2
hart            : 2
isa             : rv64imafdcsu
mmu             : sv48

processor       : 3
hart            : 3
isa             : rv64imafdcsu
mmu             : sv48
```

No support for containerd yet.

Tested on macOS 12.3.1 Intel with QEMU 6.2.0_1 (Homebrew).
Took 200 seconds for completion of the entire boot script.


afbjorklund: Nice!

This probably makes more sense long-term, than the PowerPC experiments that I did...
(someone was wondering if lima supported `ppc64le` and `s390x`, since ubuntu does)

afbjorklund: There only seems to be one board available ?

https://www.sifive.com/boards/hifive-unmatched

At least when glancing through the docs:

https://wiki.ubuntu.com/RISC-V


Patch:
diff --git a/Makefile b/Makefile
index e858c142..26fc6f4e 100644
--- a/Makefile
+++ b/Makefile
@@ -22,7 +22,8 @@ binaries: clean \
 	_output/bin/limactl \
 	_output/bin/nerdctl.lima \
 	_output/share/lima/lima-guestagent.Linux-x86_64 \
-	_output/share/lima/lima-guestagent.Linux-aarch64
+	_output/share/lima/lima-guestagent.Linux-aarch64 \
+	_output/share/lima/lima-guestagent.Linux-riscv64
 	cp -aL examples _output/share/lima
 	mkdir -p _output/share/doc/lima
 	cp -aL *.md LICENSE docs _output/share/doc/lima
@@ -55,6 +56,11 @@ _output/share/lima/lima-guestagent.Linux-aarch64:
 	GOOS=linux GOARCH=arm64 CGO_ENABLED=0 $(GO_BUILD) -o $@ ./cmd/lima-guestagent
 	chmod 644 $@
 
+.PHONY: _output/share/lima/lima-guestagent.Linux-riscv64
+_output/share/lima/lima-guestagent.Linux-riscv64:
+	GOOS=linux GOARCH=riscv64 CGO_ENABLED=0 $(GO_BUILD) -o $@ ./cmd/lima-guestagent
+	chmod 644 $@
+
 .PHONY: diagrams
 diagrams: docs/lima-sequence-diagram.png
 docs/lima-sequence-diagram.png: docs/lima-sequence-diagram.puml
diff --git a/docs/internal.md b/docs/internal.md
index 27b2f66f..d8de4e2c 100644
--- a/docs/internal.md
+++ b/docs/internal.md
@@ -36,6 +36,11 @@ disk:
 - `basedisk`: the base image
 - `diffdisk`: the diff image (QCOW2)
 
+kernel:
+- `kernel`: the kernel
+- `kernel.cmdline`: the kernel cmdline
+- `initrd`: the initrd
+
 QEMU:
 - `qemu.pid`: QEMU PID
 - `qmp.sock`: QMP socket
diff --git a/examples/README.md b/examples/README.md
index 0b13b143..4de9a0b6 100644
--- a/examples/README.md
+++ b/examples/README.md
@@ -35,6 +35,7 @@ Others:
 - [`vmnet.yaml`](./vmnet.yaml): ⭐enable [`vmnet.framework`](../docs/network.md)
 - [`deprecated/centos-7.yaml`](./deprecated/centos-7.yaml): [deprecated] CentOS 7
 - [`experimental/9p.yaml`](experimental/9p.yaml): [experimental] use 9p mount type
+- [`experimental/riscv64.yaml`](experimental/riscv64.yaml): [experimental] RISC-V
 
 ## Tier 1
 
diff --git a/examples/experimental/riscv64.yaml b/examples/experimental/riscv64.yaml
new file mode 100644
index 00000000..766b4eaa
--- /dev/null
+++ b/examples/experimental/riscv64.yaml
@@ -0,0 +1,21 @@
+# This example requires Lima v0.11.0 or later.
+
+arch: "riscv64"
+images:
+- location: "https://cloud-images.ubuntu.com/releases/22.04/release-20220420/ubuntu-22.04-server-cloudimg-riscv64.img"
+  digest: "sha256:6315140c5f6ab773bf135cff84c565ddaa21be25f80f845826bfb1c7df739417"
+  kernel:
+    # Extracted from http://http.us.debian.org/debian/pool/main/u/u-boot/u-boot-qemu_2022.04+dfsg-2_all.deb (GPL-2.0)
+    location: "https://github.com/lima-vm/u-boot-qemu-mirror/releases/download/2022.04%2Bdfsg-2/qemu-riscv64_smode_uboot.elf"
+    digest: "sha256:d250ede9b4d0ea1871714395edd38a7eb2bc8425b697673cfd15e62e7ee4529c"
+
+mounts:
+- location: "~"
+- location: "/tmp/lima"
+  writable: true
+mountType: "9p"
+
+# We do not have riscv64 binaries of containerd
+containerd:
+  system: false
+  user: false
diff --git a/pkg/limayaml/defaults.go b/pkg/limayaml/defaults.go
index a41610cd..beeb7b48 100644
--- a/pkg/limayaml/defaults.go
+++ b/pkg/limayaml/defaults.go
@@ -45,6 +45,7 @@ func defaultContainerdArchives() []File {
 			Arch:     AARCH64,
 			Digest:   "sha256:d07c5213a6789eb1d3d07febaa982a043611e8205264066388c43317924cf446",
 		},
+		// No riscv64
 	}
 }
 
@@ -89,12 +90,19 @@ func FillDefault(y, d, o *LimaYAML, filePath string) {
 		if img.Arch == "" {
 			img.Arch = *y.Arch
 		}
+		if img.Kernel != nil && img.Kernel.Arch == "" {
+			img.Kernel.Arch = img.Arch
+		}
+		if img.Initrd != nil && img.Initrd.Arch == "" {
+			img.Initrd.Arch = img.Arch
+		}
 	}
 
 	cpuType := map[Arch]string{
 		AARCH64: "cortex-a72",
 		// Since https://github.com/lima-vm/lima/pull/494, we use qemu64 cpu for better emulation of x86_64.
-		X8664: "qemu64",
+		X8664:   "qemu64",
+		RISCV64: "rv64", // FIXME: what is the right choice for riscv64?
 	}
 	for arch := range cpuType {
 		if IsNativeArch(arch) {
@@ -563,6 +571,8 @@ func NewArch(arch string) Arch {
 		return X8664
 	case "arm64":
 		return AARCH64
+	case "riscv64":
+		return RISCV64
 	default:
 		logrus.Warnf("Unknown arch: %s", arch)
 		return arch
@@ -579,7 +589,8 @@ func ResolveArch(s *string) Arch {
 func IsNativeArch(arch Arch) bool {
 	nativeX8664 := arch == X8664 && runtime.GOARCH == "amd64"
 	nativeAARCH64 := arch == AARCH64 && runtime.GOARCH == "arm64"
-	return nativeX8664 || nativeAARCH64
+	nativeRISCV64 := arch == RISCV64 && runtime.GOARCH == "riscv64"
+	return nativeX8664 || nativeAARCH64 || nativeRISCV64
 }
 
 func Cname(host string) string {
diff --git a/pkg/limayaml/limayaml.go b/pkg/limayaml/limayaml.go
index c7ba62f0..7d2fa084 100644
--- a/pkg/limayaml/limayaml.go
+++ b/pkg/limayaml/limayaml.go
@@ -8,7 +8,7 @@ import (
 
 type LimaYAML struct {
 	Arch              *Arch             `yaml:"arch,omitempty" json:"arch,omitempty"`
-	Images            []File            `yaml:"images" json:"images"` // REQUIRED
+	Images            []Image           `yaml:"images" json:"images"` // REQUIRED
 	CPUType           map[Arch]string   `yaml:"cpuType,omitempty" json:"cpuType,omitempty"`
 	CPUs              *int              `yaml:"cpus,omitempty" json:"cpus,omitempty"`
 	Memory            *string           `yaml:"memory,omitempty" json:"memory,omitempty"` // go-units.RAMInBytes
@@ -39,6 +39,7 @@ type MountType = string
 const (
 	X8664   Arch = "x86_64"
 	AARCH64 Arch = "aarch64"
+	RISCV64 Arch = "riscv64"
 
 	REVSSHFS MountType = "reverse-sshfs"
 	NINEP    MountType = "9p"
@@ -50,6 +51,17 @@ type File struct {
 	Digest   digest.Digest `yaml:"digest,omitempty" json:"digest,omitempty"`
 }
 
+type Kernel struct {
+	File    `yaml:",inline"`
+	Cmdline string `yaml:"cmdline,omitempty" json:"cmdline,omitempty"`
+}
+
+type Image struct {
+	File   `yaml:",inline"`
+	Kernel *Kernel `yaml:"kernel,omitempty" json:"kernel,omitempty"`
+	Initrd *File   `yaml:"initrd,omitempty" json:"initrd,omitempty"`
+}
+
 type Mount struct {
 	Location string `yaml:"location" json:"location"` // REQUIRED
 	Writable *bool  `yaml:"writable,omitempty" json:"writable,omitempty"`
diff --git a/pkg/limayaml/validate.go b/pkg/limayaml/validate.go
index 6c8ada3e..4c5d8668 100644
--- a/pkg/limayaml/validate.go
+++ b/pkg/limayaml/validate.go
@@ -18,41 +18,69 @@ import (
 	"github.com/sirupsen/logrus"
 )
 
+func validateFileObject(f File, fieldName string) error {
+	if !strings.Contains(f.Location, "://") {
+		if _, err := localpathutil.Expand(f.Location); err != nil {
+			return fmt.Errorf("field `%s.location` refers to an invalid local file path: %q: %w", fieldName, f.Location, err)
+		}
+		// f.Location does NOT need to be accessible, so we do NOT check os.Stat(f.Location)
+	}
+	switch f.Arch {
+	case X8664, AARCH64, RISCV64:
+	default:
+		return fmt.Errorf("field `arch` must be %q, %q, or %q; got %q", X8664, AARCH64, RISCV64, f.Arch)
+	}
+	if f.Digest != "" {
+		if !f.Digest.Algorithm().Available() {
+			return fmt.Errorf("field `%s.digest` refers to an unavailable digest algorithm", fieldName)
+		}
+		if err := f.Digest.Validate(); err != nil {
+			return fmt.Errorf("field `%s.digest` is invalid: %s: %w", fieldName, f.Digest.String(), err)
+		}
+	}
+	return nil
+}
+
 func Validate(y LimaYAML, warn bool) error {
 	switch *y.Arch {
-	case X8664, AARCH64:
+	case X8664, AARCH64, RISCV64:
 	default:
-		return fmt.Errorf("field `arch` must be %q or %q , got %q", X8664, AARCH64, *y.Arch)
+		return fmt.Errorf("field `arch` must be %q, %q, or %q; got %q", X8664, AARCH64, RISCV64, *y.Arch)
 	}
 
 	if len(y.Images) == 0 {
 		return errors.New("field `images` must be set")
 	}
 	for i, f := range y.Images {
-		if !strings.Contains(f.Location, "://") {
-			if _, err := localpathutil.Expand(f.Location); err != nil {
-				return fmt.Errorf("field `images[%d].location` refers to an invalid local file path: %q: %w", i, f.Location, err)
-			}
-			// f.Location does NOT need to be accessible, so we do NOT check os.Stat(f.Location)
+		if err := validateFileObject(f.File, fmt.Sprintf("images[%d]", i)); err != nil {
+			return err
 		}
-		switch f.Arch {
-		case X8664, AARCH64:
-		default:
-			return fmt.Errorf("field `images.arch` must be %q or %q, got %q", X8664, AARCH64, f.Arch)
+		if f.Kernel != nil {
+			if err := validateFileObject(f.Kernel.File, fmt.Sprintf("images[%d].kernel", i)); err != nil {
+				return err
+			}
+			if f.Kernel.Arch != *y.Arch {
+				return fmt.Errorf("images[%d].kernel has unexpected architecture %q, must be %q", i, f.Kernel.Arch, *y.Arch)
+			}
+		} else if f.Arch == RISCV64 {
+			return errors.New("riscv64 needs the kernel (e.g., \"uboot.elf\") to be specified")
 		}
-		if f.Digest != "" {
-			if !f.Digest.Algorithm().Available() {
-				return fmt.Errorf("field `images[%d].digest` refers to an unavailable digest algorithm", i)
+		if f.Initrd != nil {
+			if err := validateFileObject(*f.Initrd, fmt.Sprintf("images[%d].initrd", i)); err != nil {
+				return err
+			}
+			if f.Kernel == nil {
+				return errors.New("initrd requires the kernel to be specified")
 			}
-			if err := f.Digest.Validate(); err != nil {
-				return fmt.Errorf("field `images[%d].digest` is invalid: %s: %w", i, f.Digest.String(), err)
+			if f.Initrd.Arch != *y.Arch {
+				return fmt.Errorf("images[%d].initrd has unexpected architecture %q, must be %q", i, f.Initrd.Arch, *y.Arch)
 			}
 		}
 	}
 
 	for arch := range y.CPUType {
 		switch arch {
-		case AARCH64, X8664:
+		case AARCH64, X8664, RISCV64:
 			// these are the only supported architectures
 		default:
 			return fmt.Errorf("field `cpuType` uses unsupported arch %q", arch)
diff --git a/pkg/qemu/qemu.go b/pkg/qemu/qemu.go
index 13e8218c..49f55f03 100644
--- a/pkg/qemu/qemu.go
+++ b/pkg/qemu/qemu.go
@@ -33,6 +33,31 @@ type Config struct {
 	SSHLocalPort int
 }
 
+func downloadFile(dest string, f limayaml.File, description string, expectedArch limayaml.Arch) error {
+	if f.Arch != expectedArch {
+		return fmt.Errorf("unsupported arch: %q", f.Arch)
+	}
+	logrus.WithField("digest", f.Digest).Infof("Attempting to download %s from %q", description, f.Location)
+	res, err := downloader.Download(dest, f.Location,
+		downloader.WithCache(),
+		downloader.WithExpectedDigest(f.Digest),
+	)
+	if err != nil {
+		return fmt.Errorf("failed to download %q: %w", f.Location, err)
+	}
+	logrus.Debugf("res.ValidatedDigest=%v", res.ValidatedDigest)
+	switch res.Status {
+	case downloader.StatusDownloaded:
+		logrus.Infof("Downloaded %s from %q", description, f.Location)
+	case downloader.StatusUsedCache:
+		logrus.Infof("Using cache %q", res.CachePath)
+	default:
+		logrus.Warnf("Unexpected result from downloader.Download(): %+v", res)
+	}
+	return nil
+}
+
+// EnsureDisk also ensures the kernel and the initrd
 func EnsureDisk(cfg Config) error {
 	diffDisk := filepath.Join(cfg.InstanceDir, filenames.DiffDisk)
 	if _, err := os.Stat(diffDisk); err == nil || !errors.Is(err, os.ErrNotExist) {
@@ -41,31 +66,34 @@ func EnsureDisk(cfg Config) error {
 	}
 
 	baseDisk := filepath.Join(cfg.InstanceDir, filenames.BaseDisk)
+	kernel := filepath.Join(cfg.InstanceDir, filenames.Kernel)
+	kernelCmdline := filepath.Join(cfg.InstanceDir, filenames.KernelCmdline)
+	initrd := filepath.Join(cfg.InstanceDir, filenames.Initrd)
 	if _, err := os.Stat(baseDisk); errors.Is(err, os.ErrNotExist) {
 		var ensuredBaseDisk bool
 		errs := make([]error, len(cfg.LimaYAML.Images))
 		for i, f := range cfg.LimaYAML.Images {
-			if f.Arch != *cfg.LimaYAML.Arch {
-				errs[i] = fmt.Errorf("unsupported arch: %q", f.Arch)
+			if err := downloadFile(baseDisk, f.File, "the image", *cfg.LimaYAML.Arch); err != nil {
+				errs[i] = err
 				continue
 			}
-			logrus.WithField("digest", f.Digest).Infof("Attempting to download the image from %q", f.Location)
-			res, err := downloader.Download(baseDisk, f.Location,
-				downloader.WithCache(),
-				downloader.WithExpectedDigest(f.Digest),
-			)
-			if err != nil {
-				errs[i] = fmt.Errorf("failed to download %q: %w", f.Location, err)
-				continue
+			if f.Kernel != nil {
+				if err := downloadFile(kernel, f.Kernel.File, "the kernel", *cfg.LimaYAML.Arch); err != nil {
+					errs[i] = err
+					continue
+				}
+				if f.Kernel.Cmdline != "" {
+					if err := os.WriteFile(kernelCmdline, []byte(f.Kernel.Cmdline), 0644); err != nil {
+						errs[i] = err
+						continue
+					}
+				}
 			}
-			logrus.Debugf("res.ValidatedDigest=%v", res.ValidatedDigest)
-			switch res.Status {
-			case downloader.StatusDownloaded:
-				logrus.Infof("Downloaded image from %q", f.Location)
-			case downloader.StatusUsedCache:
-				logrus.Infof("Using cache %q", res.CachePath)
-			default:
-				logrus.Warnf("Unexpected result from downloader.Download(): %+v", res)
+			if f.Initrd != nil {
+				if err := downloadFile(initrd, *f.Initrd, "the initrd", *cfg.LimaYAML.Arch); err != nil {
+					errs[i] = err
+					continue
+				}
 			}
 			ensuredBaseDisk = true
 			break
@@ -360,6 +388,9 @@ func Cmdline(cfg Config) (string, []string, error) {
 			machine += ",highmem=off"
 		}
 		args = appendArgsIfNoConflict(args, "-machine", machine)
+	case limayaml.RISCV64:
+		machine := "virt,accel=" + accel
+		args = appendArgsIfNoConflict(args, "-machine", machine)
 	}
 
 	// SMP
@@ -372,7 +403,7 @@ func Cmdline(cfg Config) (string, []string, error) {
 		logrus.Warnf("field `firmware.legacyBIOS` is not supported for architecture %q, ignoring", *y.Arch)
 		legacyBIOS = false
 	}
-	if !legacyBIOS {
+	if !legacyBIOS && *y.Arch != limayaml.RISCV64 {
 		firmware, err := getFirmware(exe, *y.Arch)
 		if err != nil {
 			return "", nil, err
@@ -380,6 +411,7 @@ func Cmdline(cfg Config) (string, []string, error) {
 		args = append(args, "-drive", fmt.Sprintf("if=pflash,format=raw,readonly=on,file=%s", firmware))
 	}
 
+	// Disk
 	baseDisk := filepath.Join(cfg.InstanceDir, filenames.BaseDisk)
 	diffDisk := filepath.Join(cfg.InstanceDir, filenames.DiffDisk)
 	isBaseDiskCDROM, err := iso9660util.IsISO9660(baseDisk)
@@ -398,7 +430,31 @@ func Cmdline(cfg Config) (string, []string, error) {
 		args = append(args, "-drive", fmt.Sprintf("file=%s,if=virtio", baseDisk))
 	}
 	// cloud-init
-	args = append(args, "-cdrom", filepath.Join(cfg.InstanceDir, filenames.CIDataISO))
+	switch *y.Arch {
+	case limayaml.RISCV64:
+		// -cdrom does not seem recognized for RISCV64
+		args = append(args,
+			"-drive", "id=cdrom0,if=none,format=raw,readonly=on,file="+filepath.Join(cfg.InstanceDir, filenames.CIDataISO),
+			"-device", "virtio-scsi-pci,id=scsi0",
+			"-device", "scsi-cd,bus=scsi0.0,drive=cdrom0")
+	default:
+		// TODO: consider using virtio cdrom for all the architectures
+		args = append(args, "-cdrom", filepath.Join(cfg.InstanceDir, filenames.CIDataISO))
+	}
+
+	// Kernel
+	kernel := filepath.Join(cfg.InstanceDir, filenames.Kernel)
+	kernelCmdline := filepath.Join(cfg.InstanceDir, filenames.KernelCmdline)
+	initrd := filepath.Join(cfg.InstanceDir, filenames.Initrd)
+	if _, err := os.Stat(kernel); err == nil {
+		args = appendArgsIfNoConflict(args, "-kernel", kernel)
+	}
+	if b, err := os.ReadFile(kernelCmdline); err == nil {
+		args = appendArgsIfNoConflict(args, "-append", string(b))
+	}
+	if _, err := os.Stat(initrd); err == nil {
+		args = appendArgsIfNoConflict(args, "-initrd", initrd)
+	}
 
 	// Network
 	args = append(args, "-netdev", fmt.Sprintf("user,id=net0,net=%s,dhcpstart=%s,hostfwd=tcp:127.0.0.1:%d-:22",
@@ -554,6 +610,11 @@ func getAccel(arch limayaml.Arch) string {
 }
 
 func getFirmware(qemuExe string, arch limayaml.Arch) (string, error) {
+	switch arch {
+	case limayaml.X8664, limayaml.AARCH64:
+	default:
+		return "", fmt.Errorf("unexpected architecture: %q", arch)
+	}
 	binDir := filepath.Dir(qemuExe)  // "/usr/local/bin"
 	localDir := filepath.Dir(binDir) // "/usr/local"
 
diff --git a/pkg/store/filenames/filenames.go b/pkg/store/filenames/filenames.go
index 68e7f99e..fc3be1de 100644
--- a/pkg/store/filenames/filenames.go
+++ b/pkg/store/filenames/filenames.go
@@ -29,6 +29,9 @@ const (
 	CIDataISO          = "cidata.iso"
 	BaseDisk           = "basedisk"
 	DiffDisk           = "diffdisk"
+	Kernel             = "kernel"
+	KernelCmdline      = "kernel.cmdline"
+	Initrd             = "initrd"
 	QemuPID            = "qemu.pid"
 	QMPSock            = "qmp.sock"
 	SerialLog          = "serial.log"


Test patch:
diff --git a/pkg/limayaml/defaults_test.go b/pkg/limayaml/defaults_test.go
index a7327a4c..671ad62e 100644
--- a/pkg/limayaml/defaults_test.go
+++ b/pkg/limayaml/defaults_test.go
@@ -51,6 +51,7 @@ func TestFillDefault(t *testing.T) {
 		CPUType: map[Arch]string{
 			AARCH64: "cortex-a72",
 			X8664:   "qemu64",
+			RISCV64: "rv64",
 		},
 		CPUs:   pointer.Int(4),
 		Memory: pointer.String("4GiB"),
@@ -211,6 +212,7 @@ func TestFillDefault(t *testing.T) {
 		CPUType: map[Arch]string{
 			AARCH64: "arm64",
 			X8664:   "amd64",
+			RISCV64: "riscv64",
 		},
 		CPUs:   pointer.Int(7),
 		Memory: pointer.String("5GiB"),
@@ -351,6 +353,7 @@ func TestFillDefault(t *testing.T) {
 		CPUType: map[Arch]string{
 			AARCH64: "uber-arm",
 			X8664:   "pentium",
+			RISCV64: "sifive-u54",
 		},
 		CPUs:   pointer.Int(12),
 		Memory: pointer.String("7GiB"),