Repository: lima-vm/lima

Issue:
Command reference should note if a command is not available on all platforms
#3425 removed the `sudoers` command from Linux and Windows.

The `start-at-login` command was never available on Windows.

The docs should note these restrictions:
* https://lima-vm.io/docs/reference/limactl_sudoers/
* https://lima-vm.io/docs/reference/limactl_start-at-login/

Also, since netlify runs the build step on Linux, I would have expected that the `sudoers` reference page would be removed now. I don't understand why it claims (in GitHub actions) there were no changes. Maybe it does not detect when a page is being deleted?

When I run `make docsy` locally on Linux, then the `sudoers` page is no longer generated.

Which means with the current setup we will not have documentation for commands that are not included on Linux. So it might be better to always add them, and have them output `The foo is not supported on this platform` than just removing them.

I think that error is less confusing to the user than

```
FATA[0000] unknown command "foo" for "limactl"
```

when the online docs claim that the command exists.

Hints from issue comments:
@AkihiroSuda: > So it might be better to always add them, and have them output The foo is not supported on this platform than just removing them.

👍 @alexandear: > Also, since netlify runs the build step on Linux, I would have expected that the sudoers reference page would be removed now.

It's removed for me:

<img width="627" alt="Image" src="https://github.com/user-attachments/assets/6fda993f-8452-4d73-91e4-61869a033ab1" />

---

In the short term, we can revert #3425


In the long term, it might be better to update the docs to display slightly different information depending on the platform. The [pkg.go.dev](https://pkg.go.dev/os) already does this:

<img width="488" alt="Image" src="https://github.com/user-attachments/assets/eef42998-c5b7-4d6c-8d0a-e4d5e209de93" />@AkihiroSuda: The pages are not updated?@AkihiroSuda: The page is now somehow generated

Solution patch:
diff --git a/Makefile b/Makefile
index 5f679955..c0b576d0 100644
--- a/Makefile
+++ b/Makefile
@@ -436,14 +436,15 @@ ifeq ($(native_compiling),true)
 	$< tmpl copy --embed-all templates/default.yaml $@
 endif
 
-schema-limayaml.json: _output/bin/limactl$(exe) default-template.yaml
+schema-limayaml.json: _output/bin/limactl$(exe) templates/default.yaml default-template.yaml
 ifeq ($(native_compiling),true)
-	$< generate-jsonschema --schemafile $@ default-template.yaml
+	# validate both the original template (with the "base" etc), and the embedded template
+	$< generate-jsonschema --schemafile $@ templates/default.yaml default-template.yaml
 endif
 
 .PHONY: check-jsonschema
-check-jsonschema: schema-limayaml.json default-template.yaml
-	check-jsonschema --schemafile schema-limayaml.json default-template.yaml
+check-jsonschema: schema-limayaml.json templates/default.yaml default-template.yaml
+	check-jsonschema --schemafile schema-limayaml.json templates/default.yaml default-template.yaml
 
 ################################################################################
 .PHONY: diagrams
diff --git a/cmd/limactl/disk.go b/cmd/limactl/disk.go
index f5d17136..44e09bdb 100644
--- a/cmd/limactl/disk.go
+++ b/cmd/limactl/disk.go
@@ -16,7 +16,7 @@ import (
 	"github.com/docker/go-units"
 	"github.com/lima-vm/go-qcow2reader"
 	"github.com/lima-vm/lima/pkg/nativeimgutil"
-	"github.com/lima-vm/lima/pkg/qemu"
+	"github.com/lima-vm/lima/pkg/qemu/imgutil"
 	"github.com/lima-vm/lima/pkg/store"
 	"github.com/lima-vm/lima/pkg/store/filenames"
 	"github.com/sirupsen/logrus"
@@ -114,7 +114,7 @@ func diskCreateAction(cmd *cobra.Command, args []string) error {
 	if format == "raw" {
 		err = nativeimgutil.CreateRawDataDisk(diskDir, int(diskSize))
 	} else {
-		err = qemu.CreateDataDisk(diskDir, format, int(diskSize))
+		err = imgutil.CreateDataDisk(diskDir, format, int(diskSize))
 	}
 	if err != nil {
 		rerr := os.RemoveAll(diskDir)
@@ -410,7 +410,7 @@ func diskResizeAction(cmd *cobra.Command, args []string) error {
 	if disk.Format == "raw" {
 		err = nativeimgutil.ResizeRawDataDisk(disk.Dir, int(diskSize))
 	} else {
-		err = qemu.ResizeDataDisk(disk.Dir, disk.Format, int(diskSize))
+		err = imgutil.ResizeDataDisk(disk.Dir, disk.Format, int(diskSize))
 	}
 	if err != nil {
 		return fmt.Errorf("failed to resize disk %q: %w", diskName, err)
diff --git a/cmd/limactl/genschema.go b/cmd/limactl/genschema.go
index 8de619fd..296db8ee 100644
--- a/cmd/limactl/genschema.go
+++ b/cmd/limactl/genschema.go
@@ -60,6 +60,18 @@ func genschemaAction(cmd *cobra.Command, args []string) error {
 		{Type: "string"},
 		{Type: "object"},
 	}
+	// allow BaseTemplates to be either string (url) or array (array)
+	schema.Definitions["BaseTemplates"].Type = "" // was: "array"
+	schema.Definitions["BaseTemplates"].OneOf = []*jsonschema.Schema{
+		{Type: "string"},
+		{Type: "array"},
+	}
+	// allow LocatorWithDigest to be either string (url) or object (struct)
+	schema.Definitions["LocatorWithDigest"].Type = "" // was: "object"
+	schema.Definitions["LocatorWithDigest"].OneOf = []*jsonschema.Schema{
+		{Type: "string"},
+		{Type: "object"},
+	}
 	properties := schema.Definitions["LimaYAML"].Properties
 	getProp(properties, "os").Enum = toAny(limayaml.OSTypes)
 	getProp(properties, "arch").Enum = toAny(limayaml.ArchTypes)
diff --git a/cmd/limactl/main.go b/cmd/limactl/main.go
index a428a9fd..d56047f5 100644
--- a/cmd/limactl/main.go
+++ b/cmd/limactl/main.go
@@ -186,10 +186,9 @@ func newApp() *cobra.Command {
 		newTunnelCommand(),
 		newTemplateCommand(),
 		newRestartCommand(),
+		newSudoersCommand(),
+		newStartAtLoginCommand(),
 	)
-	for _, cmd := range additionalAdvancedCommands() {
-		rootCmd.AddCommand(cmd)
-	}
 
 	return rootCmd
 }
diff --git a/cmd/limactl/main_darwin.go b/cmd/limactl/main_darwin.go
deleted file mode 100644
index f3849d20..00000000
--- a/cmd/limactl/main_darwin.go
+++ /dev/null
@@ -1,13 +0,0 @@
-// SPDX-FileCopyrightText: Copyright The Lima Authors
-// SPDX-License-Identifier: Apache-2.0
-
-package main
-
-import "github.com/spf13/cobra"
-
-func additionalAdvancedCommands() []*cobra.Command {
-	return []*cobra.Command{
-		newSudoersCommand(),
-		startAtLoginCommand(),
-	}
-}
diff --git a/cmd/limactl/main_linux.go b/cmd/limactl/main_linux.go
deleted file mode 100644
index f09c3ea7..00000000
--- a/cmd/limactl/main_linux.go
+++ /dev/null
@@ -1,12 +0,0 @@
-// SPDX-FileCopyrightText: Copyright The Lima Authors
-// SPDX-License-Identifier: Apache-2.0
-
-package main
-
-import "github.com/spf13/cobra"
-
-func additionalAdvancedCommands() []*cobra.Command {
-	return []*cobra.Command{
-		startAtLoginCommand(),
-	}
-}
diff --git a/cmd/limactl/main_windows.go b/cmd/limactl/main_windows.go
deleted file mode 100644
index bf3bba33..00000000
--- a/cmd/limactl/main_windows.go
+++ /dev/null
@@ -1,10 +0,0 @@
-// SPDX-FileCopyrightText: Copyright The Lima Authors
-// SPDX-License-Identifier: Apache-2.0
-
-package main
-
-import "github.com/spf13/cobra"
-
-func additionalAdvancedCommands() []*cobra.Command {
-	return nil
-}
diff --git a/cmd/limactl/start-at-login.go b/cmd/limactl/start-at-login.go
new file mode 100644
index 00000000..b7ca2aaf
--- /dev/null
+++ b/cmd/limactl/start-at-login.go
@@ -0,0 +1,30 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
+package main
+
+import (
+	"github.com/spf13/cobra"
+)
+
+func newStartAtLoginCommand() *cobra.Command {
+	startAtLoginCommand := &cobra.Command{
+		Use:               "start-at-login INSTANCE",
+		Short:             "Register/Unregister an autostart file for the instance",
+		Args:              WrapArgsError(cobra.MaximumNArgs(1)),
+		RunE:              startAtLoginAction,
+		ValidArgsFunction: startAtLoginComplete,
+		GroupID:           advancedCommand,
+	}
+
+	startAtLoginCommand.Flags().Bool(
+		"enabled", true,
+		"Automatically start the instance when the user logs in",
+	)
+
+	return startAtLoginCommand
+}
+
+func startAtLoginComplete(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
+	return bashCompleteInstanceNames(cmd)
+}
diff --git a/cmd/limactl/start-at-login_unix.go b/cmd/limactl/start-at-login_unix.go
index b6e0e0ae..6b4b189d 100644
--- a/cmd/limactl/start-at-login_unix.go
+++ b/cmd/limactl/start-at-login_unix.go
@@ -16,24 +16,6 @@ import (
 	"github.com/spf13/cobra"
 )
 
-func startAtLoginCommand() *cobra.Command {
-	startAtLoginCommand := &cobra.Command{
-		Use:               "start-at-login INSTANCE",
-		Short:             "Register/Unregister an autostart file for the instance",
-		Args:              WrapArgsError(cobra.MaximumNArgs(1)),
-		RunE:              startAtLoginAction,
-		ValidArgsFunction: startAtLoginComplete,
-		GroupID:           advancedCommand,
-	}
-
-	startAtLoginCommand.Flags().Bool(
-		"enabled", true,
-		"Automatically start the instance when the user logs in",
-	)
-
-	return startAtLoginCommand
-}
-
 func startAtLoginAction(cmd *cobra.Command, args []string) error {
 	instName := DefaultInstanceName
 	if len(args) > 0 {
@@ -71,7 +53,3 @@ func startAtLoginAction(cmd *cobra.Command, args []string) error {
 
 	return nil
 }
-
-func startAtLoginComplete(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
-	return bashCompleteInstanceNames(cmd)
-}
diff --git a/cmd/limactl/start-at-login_windows.go b/cmd/limactl/start-at-login_windows.go
new file mode 100644
index 00000000..5112dff8
--- /dev/null
+++ b/cmd/limactl/start-at-login_windows.go
@@ -0,0 +1,14 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
+package main
+
+import (
+	"errors"
+
+	"github.com/spf13/cobra"
+)
+
+func startAtLoginAction(_ *cobra.Command, _ []string) error {
+	return errors.New("start-at-login command is only supported on macOS and Linux right now")
+}
diff --git a/cmd/limactl/sudoers.go b/cmd/limactl/sudoers.go
new file mode 100644
index 00000000..7478312e
--- /dev/null
+++ b/cmd/limactl/sudoers.go
@@ -0,0 +1,41 @@
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
+package main
+
+import (
+	"fmt"
+
+	"github.com/lima-vm/lima/pkg/networks"
+	"github.com/spf13/cobra"
+)
+
+const socketVMNetURL = "https://lima-vm.io/docs/config/network/vmnet/#socket_vmnet"
+
+// newSudoersCommand is specific to macOS, but the help message is
+// compiled on Linux too, as depended by `make docsy`.
+// https://github.com/lima-vm/lima/issues/3436
+func newSudoersCommand() *cobra.Command {
+	sudoersCommand := &cobra.Command{
+		Use: "sudoers [--check [SUDOERSFILE-TO-CHECK]]",
+		Example: `
+To generate the /etc/sudoers.d/lima file:
+$ limactl sudoers | sudo tee /etc/sudoers.d/lima
+
+To validate the existing /etc/sudoers.d/lima file:
+$ limactl sudoers --check /etc/sudoers.d/lima
+`,
+		Short: "Generate the content of the /etc/sudoers.d/lima file",
+		Long: fmt.Sprintf(`Generate the content of the /etc/sudoers.d/lima file for enabling vmnet.framework support (socket_vmnet) on macOS.
+The content is written to stdout, NOT to the file.
+This command must not run as the root user.
+See %s for the usage.`, socketVMNetURL),
+		Args:    WrapArgsError(cobra.MaximumNArgs(1)),
+		RunE:    sudoersAction,
+		GroupID: advancedCommand,
+	}
+	cfgFile, _ := networks.ConfigFile()
+	sudoersCommand.Flags().Bool("check", false,
+		fmt.Sprintf("check that the sudoers file is up-to-date with %q", cfgFile))
+	return sudoersCommand
+}
diff --git a/cmd/limactl/sudoers_darwin.go b/cmd/limactl/sudoers_darwin.go
index dfe3ba70..96fb265b 100644
--- a/cmd/limactl/sudoers_darwin.go
+++ b/cmd/limactl/sudoers_darwin.go
@@ -13,33 +13,6 @@ import (
 	"github.com/spf13/cobra"
 )
 
-const networksURL = "https://lima-vm.io/docs/config/network/#socket_vmnet"
-
-func newSudoersCommand() *cobra.Command {
-	sudoersCommand := &cobra.Command{
-		Use: "sudoers [--check [SUDOERSFILE-TO-CHECK]]",
-		Example: `
-To generate the /etc/sudoers.d/lima file:
-$ limactl sudoers | sudo tee /etc/sudoers.d/lima
-
-To validate the existing /etc/sudoers.d/lima file:
-$ limactl sudoers --check /etc/sudoers.d/lima
-`,
-		Short: "Generate the content of the /etc/sudoers.d/lima file",
-		Long: fmt.Sprintf(`Generate the content of the /etc/sudoers.d/lima file for enabling vmnet.framework support.
-The content is written to stdout, NOT to the file.
-This command must not run as the root user.
-See %s for the usage.`, networksURL),
-		Args:    WrapArgsError(cobra.MaximumNArgs(1)),
-		RunE:    sudoersAction,
-		GroupID: advancedCommand,
-	}
-	cfgFile, _ := networks.ConfigFile()
-	sudoersCommand.Flags().Bool("check", false,
-		fmt.Sprintf("check that the sudoers file is up-to-date with %q", cfgFile))
-	return sudoersCommand
-}
-
 func sudoersAction(cmd *cobra.Command, args []string) error {
 	nwCfg, err := networks.LoadConfig()
 	if err != nil {
@@ -47,7 +20,7 @@ func sudoersAction(cmd *cobra.Command, args []string) error {
 	}
 	// Make sure the current network configuration is secure
 	if err := nwCfg.Validate(); err != nil {
-		logrus.Infof("Please check %s for more information.", networksURL)
+		logrus.Infof("Please check %s for more information.", socketVMNetURL)
 		return err
 	}
 	check, err := cmd.Flags().GetBool("check")
diff --git a/cmd/limactl/sudoers_nodarwin.go b/cmd/limactl/sudoers_nodarwin.go
new file mode 100644
index 00000000..07f60a50
--- /dev/null
+++ b/cmd/limactl/sudoers_nodarwin.go
@@ -0,0 +1,16 @@
+//go:build !darwin
+
+// SPDX-FileCopyrightText: Copyright The Lima Authors
+// SPDX-License-Identifier: Apache-2.0
+
+package main
+
+import (
+	"errors"
+
+	"github.com/spf13/cobra"
+)
+
+func sudoersAction(_ *cobra.Command, _ []string) error {
+	return errors.New("sudoers command is only supported on macOS right now")
+}
diff --git a/go.mod b/go.mod
index 8229d908..d966f9d4 100644
--- a/go.mod
+++ b/go.mod
@@ -51,9 +51,9 @@ require (
 	google.golang.org/protobuf v1.36.6 // gomodjail:unconfined
 	gopkg.in/op/go-logging.v1 v1.0.0-20160211212156-b2cb9fa56473
 	gotest.tools/v3 v3.5.2
-	k8s.io/api v0.32.3
-	k8s.io/apimachinery v0.32.3
-	k8s.io/client-go v0.32.3
+	k8s.io/api v0.32.4
+	k8s.io/apimachinery v0.32.4
+	k8s.io/client-go v0.32.4
 )
 
 require (
diff --git a/go.sum b/go.sum
index 25c3628b..b4e6a218 100644
--- a/go.sum
+++ b/go.sum
@@ -438,12 +438,12 @@ gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
 gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
 gvisor.dev/gvisor v0.0.0-20240916094835-a174eb65023f h1:O2w2DymsOlM/nv2pLNWCMCYOldgBBMkD7H0/prN5W2k=
 gvisor.dev/gvisor v0.0.0-20240916094835-a174eb65023f/go.mod h1:sxc3Uvk/vHcd3tj7/DHVBoR5wvWT/MmRq2pj7HRJnwU=
-k8s.io/api v0.32.3 h1:Hw7KqxRusq+6QSplE3NYG4MBxZw1BZnq4aP4cJVINls=
-k8s.io/api v0.32.3/go.mod h1:2wEDTXADtm/HA7CCMD8D8bK4yuBUptzaRhYcYEEYA3k=
-k8s.io/apimachinery v0.32.3 h1:JmDuDarhDmA/Li7j3aPrwhpNBA94Nvk5zLeOge9HH1U=
-k8s.io/apimachinery v0.32.3/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE=
-k8s.io/client-go v0.32.3 h1:RKPVltzopkSgHS7aS98QdscAgtgah/+zmpAogooIqVU=
-k8s.io/client-go v0.32.3/go.mod h1:3v0+3k4IcT9bXTc4V2rt+d2ZPPG700Xy6Oi0Gdl2PaY=
+k8s.io/api v0.32.4 h1:kw8Y/G8E7EpNy7gjB8gJZl3KJkNz8HM2YHrZPtAZsF4=
+k8s.io/api v0.32.4/go.mod h1:5MYFvLvweRhyKylM3Es/6uh/5hGp0dg82vP34KifX4g=
+k8s.io/apimachinery v0.32.4 h1:8EEksaxA7nd7xWJkkwLDN4SvWS5ot9g6Z/VZb3ju25I=
+k8s.io/apimachinery v0.32.4/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE=
+k8s.io/client-go v0.32.4 h1:zaGJS7xoYOYumoWIFXlcVrsiYioRPrXGO7dBfVC5R6M=
+k8s.io/client-go v0.32.4/go.mod h1:k0jftcyYnEtwlFW92xC7MTtFv5BNcZBr+zn9jPlT9Ic=
 k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
 k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
 k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJJ4JRdzg3+O6e8I+e+8T5Y=
diff --git a/pkg/limayaml/limayaml.go b/pkg/limayaml/limayaml.go
index 4082b82d..d72e5b60 100644
--- a/pkg/limayaml/limayaml.go
+++ b/pkg/limayaml/limayaml.go
@@ -10,13 +10,13 @@ import (
 )
 
 type LimaYAML struct {
-	Base                  BaseTemplates `yaml:"base,omitempty" json:"base,omitempty" jsonschema:"nullable"`
+	Base                  BaseTemplates `yaml:"base,omitempty" json:"base,omitempty"`
 	MinimumLimaVersion    *string       `yaml:"minimumLimaVersion,omitempty" json:"minimumLimaVersion,omitempty" jsonschema:"nullable"`
 	VMType                *VMType       `yaml:"vmType,omitempty" json:"vmType,omitempty" jsonschema:"nullable"`
 	VMOpts                VMOpts        `yaml:"vmOpts,omitempty" json:"vmOpts,omitempty"`
 	OS                    *OS           `yaml:"os,omitempty" json:"os,omitempty" jsonschema:"nullable"`
 	Arch                  *Arch         `yaml:"arch,omitempty" json:"arch,omitempty" jsonschema:"nullable"`
-	Images                []Image       `yaml:"images" json:"images"` // REQUIRED
+	Images                []Image       `yaml:"images,omitempty" json:"images,omitempty" jsonschema:"nullable"`
 	CPUType               CPUType       `yaml:"cpuType,omitempty" json:"cpuType,omitempty" jsonschema:"nullable"`
 	CPUs                  *int          `yaml:"cpus,omitempty" json:"cpus,omitempty" jsonschema:"nullable"`
 	Memory                *string       `yaml:"memory,omitempty" json:"memory,omitempty" jsonschema:"nullable"` // go-units.RAMInBytes
@@ -58,7 +58,7 @@ type BaseTemplates []LocatorWithDigest
 
 type LocatorWithDigest struct {
 	URL    string  `yaml:"url" json:"url"`
-	Digest *string `yaml:"digest,omitempty" json:"digest,omitempty" jsonschema:"nullable"` // TODO currently unused
+	Digest *string `yaml:"digest,omitempty" json:"digest,omitempty"` // TODO currently unused
 }
 
 type (
@@ -229,7 +229,7 @@ const (
 type Provision struct {
 	Mode                            ProvisionMode      `yaml:"mode,omitempty" json:"mode,omitempty" jsonschema:"default=system"`
 	SkipDefaultDependencyResolution *bool              `yaml:"skipDefaultDependencyResolution,omitempty" json:"skipDefaultDependencyResolution,omitempty"`
-	Script                          string             `yaml:"script" json:"script"`
+	Script                          string             `yaml:"script,omitempty" json:"script,omitempty"`
 	File                            *LocatorWithDigest `yaml:"file,omitempty" json:"file,omitempty" jsonschema:"nullable"`
 	Playbook                        string             `yaml:"playbook,omitempty" json:"playbook,omitempty"` // DEPRECATED
 	// All ProvisionData fields must be nil unless Mode is ProvisionModeData
diff --git a/pkg/limayaml/marshal.go b/pkg/limayaml/marshal.go
index 0380fa9b..70caee2f 100644
--- a/pkg/limayaml/marshal.go
+++ b/pkg/limayaml/marshal.go
@@ -45,11 +45,6 @@ func unmarshalBaseTemplates(dst *BaseTemplates, b []byte) error {
 		*dst = BaseTemplates{LocatorWithDigest{URL: s}}
 		return nil
 	}
-	var locator LocatorWithDigest
-	if err := yaml.Unmarshal(b, &locator); err == nil {
-		*dst = BaseTemplates{locator}
-		return nil
-	}
 	return yaml.UnmarshalWithOptions(b, dst, yaml.CustomUnmarshaler[LocatorWithDigest](unmarshalLocatorWithDigest))
 }
 
diff --git a/pkg/qemu/imgutil/imgutil.go b/pkg/qemu/imgutil/imgutil.go
index 2fffbfd1..688c5787 100644
--- a/pkg/qemu/imgutil/imgutil.go
+++ b/pkg/qemu/imgutil/imgutil.go
@@ -6,9 +6,15 @@ package imgutil
 import (
 	"bytes"
 	"encoding/json"
+	"errors"
 	"fmt"
+	"io/fs"
+	"os"
 	"os/exec"
+	"path/filepath"
+	"strconv"
 
+	"github.com/lima-vm/lima/pkg/store/filenames"
 	"github.com/sirupsen/logrus"
 )
 
@@ -22,6 +28,32 @@ type InfoFormatSpecific struct {
 	Data json.RawMessage `json:"data,omitempty"` // since QEMU 1.7
 }
 
+func CreateDataDisk(dir, format string, size int) error {
+	dataDisk := filepath.Join(dir, filenames.DataDisk)
+	if _, err := os.Stat(dataDisk); err == nil || !errors.Is(err, fs.ErrNotExist) {
+		// datadisk already exists
+		return err
+	}
+
+	args := []string{"create", "-f", format, dataDisk, strconv.Itoa(size)}
+	cmd := exec.Command("qemu-img", args...)
+	if out, err := cmd.CombinedOutput(); err != nil {
+		return fmt.Errorf("failed to run %v: %q: %w", cmd.Args, string(out), err)
+	}
+	return nil
+}
+
+func ResizeDataDisk(dir, format string, size int) error {
+	dataDisk := filepath.Join(dir, filenames.DataDisk)
+
+	args := []string{"resize", "-f", format, dataDisk, strconv.Itoa(size)}
+	cmd := exec.Command("qemu-img", args...)
+	if out, err := cmd.CombinedOutput(); err != nil {
+		return fmt.Errorf("failed to run %v: %q: %w", cmd.Args, string(out), err)
+	}
+	return nil
+}
+
 func (sp *InfoFormatSpecific) Qcow2() *InfoFormatSpecificDataQcow2 {
 	if sp.Type != "qcow2" {
 		return nil
diff --git a/pkg/qemu/qemu.go b/pkg/qemu/qemu.go
index 4211d2e6..b6ede13d 100644
--- a/pkg/qemu/qemu.go
+++ b/pkg/qemu/qemu.go
@@ -127,32 +127,6 @@ func EnsureDisk(ctx context.Context, cfg Config) error {
 	return nil
 }
 
-func CreateDataDisk(dir, format string, size int) error {
-	dataDisk := filepath.Join(dir, filenames.DataDisk)
-	if _, err := os.Stat(dataDisk); err == nil || !errors.Is(err, fs.ErrNotExist) {
-		// datadisk already exists
-		return err
-	}
-
-	args := []string{"create", "-f", format, dataDisk, strconv.Itoa(size)}
-	cmd := exec.Command("qemu-img", args...)
-	if out, err := cmd.CombinedOutput(); err != nil {
-		return fmt.Errorf("failed to run %v: %q: %w", cmd.Args, string(out), err)
-	}
-	return nil
-}
-
-func ResizeDataDisk(dir, format string, size int) error {
-	dataDisk := filepath.Join(dir, filenames.DataDisk)
-
-	args := []string{"resize", "-f", format, dataDisk, strconv.Itoa(size)}
-	cmd := exec.Command("qemu-img", args...)
-	if out, err := cmd.CombinedOutput(); err != nil {
-		return fmt.Errorf("failed to run %v: %q: %w", cmd.Args, string(out), err)
-	}
-	return nil
-}
-
 func newQmpClient(cfg Config) (*qmp.SocketMonitor, error) {
 	qmpSock := filepath.Join(cfg.InstanceDir, filenames.QMPSock)
 	qmpClient, err := qmp.NewSocketMonitor("unix", qmpSock, 5*time.Second)



Test patch:
diff --git a/pkg/limatmpl/abs_test.go b/pkg/limatmpl/abs_test.go
index b4f192f6..fbf7c19b 100644
--- a/pkg/limatmpl/abs_test.go
+++ b/pkg/limatmpl/abs_test.go
@@ -35,9 +35,9 @@ var useAbsLocatorsTestCases = []useAbsLocatorsTestCase{
 	{
 		"Flow style array of one base template",
 		"template://foo",
-		`base: {url: bar.yaml, digest: deadbeef}`,
+		`base: [{url: bar.yaml, digest: deadbeef}]`,
 		// not sure why the quotes around the URL were added; maybe because we don't copy the style from the source
-		`base: {url: 'template://bar.yaml', digest: deadbeef}`,
+		`base: [{url: 'template://bar.yaml', digest: deadbeef}]`,
 	},
 	{
 		"Flow style array of sequence of two base URLs",
diff --git a/pkg/limatmpl/embed_test.go b/pkg/limatmpl/embed_test.go
index a8e19291..e1db9ba2 100644
--- a/pkg/limatmpl/embed_test.go
+++ b/pkg/limatmpl/embed_test.go
@@ -374,13 +374,7 @@ provision:
 		`provision: [{script: "#! my script"}]`,
 	},
 	{
-		"ERROR base digest is not yet implemented (1)",
-		"",
-		"base: {url: base.yaml, digest: deafbad}",
-		"not yet implemented",
-	},
-	{
-		"ERROR base digest is not yet implemented (2)",
+		"ERROR base digest is not yet implemented",
 		"",
 		"base: [{url: base.yaml, digest: deafbad}]",
 		"not yet implemented",
diff --git a/pkg/limayaml/limayaml_test.go b/pkg/limayaml/limayaml_test.go
index a0320232..39c929ea 100644
--- a/pkg/limayaml/limayaml_test.go
+++ b/pkg/limayaml/limayaml_test.go
@@ -17,7 +17,7 @@ func dumpJSON(t *testing.T, d any) string {
 	return string(b)
 }
 
-const emptyYAML = "images: []\n"
+const emptyYAML = "{}\n"
 
 func TestEmptyYAML(t *testing.T) {
 	var y LimaYAML
@@ -27,7 +27,7 @@ func TestEmptyYAML(t *testing.T) {
 	assert.Equal(t, string(b), emptyYAML)
 }
 
-const defaultYAML = "images: []\n"
+const defaultYAML = "{}\n"
 
 func TestDefaultYAML(t *testing.T) {
 	bytes, err := os.ReadFile("default.yaml")
diff --git a/pkg/limayaml/marshal_test.go b/pkg/limayaml/marshal_test.go
index 29f2f72c..0f77b11f 100644
--- a/pkg/limayaml/marshal_test.go
+++ b/pkg/limayaml/marshal_test.go
@@ -28,7 +28,6 @@ func TestMarshalTilde(t *testing.T) {
 	// yaml will load ~ (or null) as null
 	// make sure that it is always quoted
 	assert.Equal(t, string(b), `---
-images: []
 mounts:
 - location: "~"
   writable: false