Repository: lima-vm/lima

Issue:
cidata: identify vmtype-specific boot scripts
https://github.com/lima-vm/lima/tree/master/pkg/cidata/cidata.TEMPLATE.d/boot

VMtype-specific boot scripts ([02-wsl2-setup.sh](https://github.com/lima-vm/lima/blob/master/pkg/cidata/cidata.TEMPLATE.d/boot/02-wsl2-setup.sh), [05-rosetta-volume.sh](https://github.com/lima-vm/lima/blob/master/pkg/cidata/cidata.TEMPLATE.d/boot/05-rosetta-volume.sh), and others?) should be consolidated to the same step.

These scripts should be eventually decoupled from the core of Lima, and should be provided by VM drivers


Hints from issue comments:
@afbjorklund: I think the wsl2-setup / lima-init can be decoupled separately, and does not have to be provided by the VM drivers.

Otherwise, it will duplicated in each driver: WSL2, Apple Containers, Kata Containers - everything without `cloud-init`

But it does need a different feature check (than checking vmType):

* https://github.com/lima-vm/lima/pull/3861

```
pkg/cidata/cidata.go:   if args.VMType == limayaml.WSL2 {
```

```
pkg/cidata/cidata.TEMPLATE.d/boot/02-wsl2-setup.sh:[ "$LIMA_CIDATA_VMTYPE" = "wsl2" ] || exit 0
```
@jandubois: Maybe drivers need a mechanism to specify/override `LIMA_CIDATA_*` variables, so they can configure builtin boot scripts (maybe @unsuman has implemented this; I haven't checked the code).@unsuman: No, actually, I have only implemented a function in the driver package(`BootScripts()`) and moved the driver-specific boot scripts under `driver/boot`(only vz for now).
[See this.](https://github.com/lima-vm/lima/pull/3901/files#diff-6a911aa7574099e18369e9477b214a031077350dedc30e60b933ad0f61e022c2R381-R392) Do you think the approach is any good?


> LIMA_CIDATA_* variables

Can you please elaborate and set some context on what it does so that I can try to add it to the recent PR?

Solution patch:
diff --git a/Makefile b/Makefile
index 90af0241..8c99b56b 100644
--- a/Makefile
+++ b/Makefile
@@ -43,16 +43,38 @@ PACKAGE := github.com/lima-vm/lima/v2
 VERSION := $(shell git describe --match 'v[0-9]*' --dirty='.m' --always --tags)
 VERSION_TRIMMED := $(VERSION:v%=%)
 
+# `DEBUG` flag to build binaries with debug information for use by `dlv exec`.
+# This implies KEEP_DWARF=1 and KEEP_SYMBOLS=1.
+DEBUG ?=
+GO_BUILD_GCFLAGS ?=
+KEEP_DWARF ?=
 KEEP_SYMBOLS ?=
+ifeq ($(DEBUG),1)
+	# Disable optimizations and inlining to make debugging easier.
+	GO_BUILD_GCFLAGS = -gcflags="all=-N -l"
+	# Keep the symbol table
+	KEEP_DWARF = 1
+	# Enable DWARF generation
+	KEEP_SYMBOLS = 1
+endif
+
+GO_BUILD_LDFLAGS_W := true
+ifeq ($(KEEP_DWARF),1)
+	GO_BUILD_LDFLAGS_W = false
+endif
+
 GO_BUILD_LDFLAGS_S := true
 ifeq ($(KEEP_SYMBOLS),1)
 	GO_BUILD_LDFLAGS_S = false
 endif
-GO_BUILD_LDFLAGS := -ldflags="-s=$(GO_BUILD_LDFLAGS_S) -w -X $(PACKAGE)/pkg/version.Version=$(VERSION)"
+# `-s`: Strip the symbol table according to the KEEP_SYMBOLS config
+# `-w`: Disable DWARF generation according to the KEEP_DWARF config
+# `-X`: Embed version information.
+GO_BUILD_LDFLAGS := -ldflags="-s=$(GO_BUILD_LDFLAGS_S) -w=$(GO_BUILD_LDFLAGS_W) -X $(PACKAGE)/pkg/version.Version=$(VERSION)"
 # `go -version -m` returns -tags with comma-separated list, because space-separated list is deprecated in go1.13.
 # converting to comma-separated list is useful for comparing with the output of `go version -m`.
 GO_BUILD_FLAG_TAGS := $(addprefix -tags=,$(shell echo "$(GO_BUILDTAGS)"|tr " " "\n"|paste -sd "," -))
-GO_BUILD := $(GO) build $(GO_BUILD_LDFLAGS) $(GO_BUILD_FLAG_TAGS)
+GO_BUILD := $(strip $(GO) build $(GO_BUILD_GCFLAGS) $(GO_BUILD_LDFLAGS) $(GO_BUILD_FLAG_TAGS))
 
 ################################################################################
 # Features
@@ -78,7 +100,9 @@ help-variables:
 	@echo  '# Variables that can be overridden.'
 	@echo
 	@echo  '- PREFIX       (directory)  : Installation prefix (default: /usr/local)'
+	@echo  '- KEEP_DWARF   (1 or 0)     : Whether to keep DWARF information (default: 0)'
 	@echo  '- KEEP_SYMBOLS (1 or 0)     : Whether to keep symbols (default: 0)'
+	@echo  '- DEBUG        (1 or 0)     : Whether to build with debug information (default: 0)'
 
 .PHONY: help-targets
 help-targets:
@@ -177,7 +201,7 @@ dependencies_for_cmd = go.mod $(call find_files_excluding_dir_and_test, ./cmd/$(
 # $(1): target binary
 extract_build_vars = $(shell \
 	($(GO) version -m $(1) 2>&- || echo $(1):) | \
-	awk 'FNR==1{print "GOVERSION="$$2}$$2~/^(CGO|GO|-ldflags|-tags).*=.+$$/{sub("^.*"$$2,$$2); print $$0}' \
+	awk 'FNR==1{print "GOVERSION="$$2}$$2~/^(CGO|GO|-gcflags|-ldflags|-tags).*=.+$$/{sub("^.*"$$2,$$2); print $$0}' \
 )
 
 # a list of keys from the GO build variables to be used for calling `go env`.
@@ -192,7 +216,7 @@ go_build_vars = $(shell \
 	$(ENVS_$(1)) $(GO) env $(2) | \
 	awk '/ /{print "\""$$0"\""; next}{print}' | \
 	for k in $(2); do read -r v && echo "$$k=$${v}"; done \
-) $(GO_BUILD_LDFLAGS) $(GO_BUILD_FLAG_TAGS)
+) $(GO_BUILD_GCFLAGS) $(GO_BUILD_LDFLAGS) $(GO_BUILD_FLAG_TAGS)
 
 # returns the difference between $(1) and $(2).
 diff = $(filter-out $(2),$(1))$(filter-out $(1),$(2))
@@ -356,16 +380,20 @@ MKDIR_TARGETS += _output/share/lima
 ################################################################################
 # _output/share/lima/templates
 TEMPLATES = $(addprefix _output/share/lima/templates/,$(filter-out experimental,$(notdir $(wildcard templates/*))))
+TEMPLATE_DEFAULTS = ${addprefix _output/share/lima/templates/_default/,$(notdir $(wildcard templates/_default/*))}
+TEMPLATE_IMAGES = $(addprefix _output/share/lima/templates/_images/,$(notdir $(wildcard templates/_images/*)))
 TEMPLATE_EXPERIMENTALS = $(addprefix _output/share/lima/templates/experimental/,$(notdir $(wildcard templates/experimental/*)))
 
 .PHONY: default_template templates template_experimentals
 default_template: _output/share/lima/templates/default.yaml
-templates: $(TEMPLATES)
+templates: $(TEMPLATES) $(TEMPLATE_DEFAULTS) $(TEMPLATE_IMAGES)
 template_experimentals: $(TEMPLATE_EXPERIMENTALS)
 
 $(TEMPLATES): | _output/share/lima/templates
+$(TEMPLATE_DEFAULTS): | _output/share/lima/templates/_default
+$(TEMPLATE_IMAGES): | _output/share/lima/templates/_images
 $(TEMPLATE_EXPERIMENTALS): | _output/share/lima/templates/experimental
-MKDIR_TARGETS += _output/share/lima/templates _output/share/lima/templates/experimental
+MKDIR_TARGETS += _output/share/lima/templates _output/share/lima/templates/_default _output/share/lima/templates/_images _output/share/lima/templates/experimental
 
 _output/share/lima/templates/%: templates/%
 	cp -aL $< $@
@@ -495,7 +523,11 @@ uninstall:
 		"$(DEST)/bin/kubectl.lima" \
 		"$(DEST)/share/man/man1/lima.1" \
 		"$(DEST)/share/man/man1/limactl"*".1" \
-		"$(DEST)/share/lima" "$(DEST)/share/doc/lima"
+		"$(DEST)/share/lima" \
+		"$(DEST)/share/doc/lima" \
+		"$(DEST)/libexec/lima/lima-driver-qemu$(exe)" \
+		"$(DEST)/libexec/lima/lima-driver-vz$(exe)" \
+		"$(DEST)/libexec/lima/lima-driver-wsl2$(exe)"
 	if [ "$$(readlink "$(DEST)/bin/nerdctl")" = "nerdctl.lima" ]; then rm "$(DEST)/bin/nerdctl"; fi
 	if [ "$$(readlink "$(DEST)/bin/apptainer")" = "apptainer.lima" ]; then rm "$(DEST)/bin/apptainer"; fi
 
diff --git a/cmd/limactl/copy.go b/cmd/limactl/copy.go
index 9d7a0055..8f3ac550 100644
--- a/cmd/limactl/copy.go
+++ b/cmd/limactl/copy.go
@@ -4,6 +4,7 @@
 package main
 
 import (
+	"context"
 	"errors"
 	"fmt"
 	"os"
@@ -26,11 +27,45 @@ const copyHelp = `Copy files between host and guest
 
 Prefix guest filenames with the instance name and a colon.
 
-Example: limactl copy default:/etc/os-release .
+Backends:
+  auto   - Automatically selects the best available backend (rsync preferred, falls back to scp)
+  rsync  - Uses rsync for faster transfers with resume capability (requires rsync on both host and guest)
+  scp    - Uses scp for reliable transfers (always available)
+
+Examples:
+  # Copy file from guest to host (auto backend)
+  limactl copy default:/etc/os-release .
+
+  # Copy file from host to guest with verbose output
+  limactl copy -v myfile.txt default:/tmp/
+
+  # Copy directory recursively using rsync backend
+  limactl copy --backend=rsync -r ./mydir default:/tmp/
+
+  # Copy using scp backend specifically
+  limactl copy --backend=scp default:/var/log/app.log ./logs/
+
+  # Copy multiple files
+  limactl copy file1.txt file2.txt default:/tmp/
 
 Not to be confused with 'limactl clone'.
 `
 
+type copyTool string
+
+const (
+	rsync copyTool = "rsync"
+	scp   copyTool = "scp"
+	auto  copyTool = "auto"
+)
+
+type copyPath struct {
+	instanceName string
+	path         string
+	isRemote     bool
+	instance     *limatype.Instance
+}
+
 func newCopyCommand() *cobra.Command {
 	copyCommand := &cobra.Command{
 		Use:     "copy SOURCE ... TARGET",
@@ -44,6 +79,7 @@ func newCopyCommand() *cobra.Command {
 
 	copyCommand.Flags().BoolP("recursive", "r", false, "Copy directories recursively")
 	copyCommand.Flags().BoolP("verbose", "v", false, "Enable verbose output")
+	copyCommand.Flags().String("backend", "auto", "Copy backend (scp|rsync|auto)")
 
 	return copyCommand
 }
@@ -60,13 +96,6 @@ func copyAction(cmd *cobra.Command, args []string) error {
 		return err
 	}
 
-	arg0, err := exec.LookPath("scp")
-	if err != nil {
-		return err
-	}
-	instances := make(map[string]*limatype.Instance)
-	scpFlags := []string{}
-	scpArgs := []string{}
 	debug, err := cmd.Flags().GetBool("debug")
 	if err != nil {
 		return err
@@ -76,62 +105,215 @@ func copyAction(cmd *cobra.Command, args []string) error {
 		verbose = true
 	}
 
-	if verbose {
-		scpFlags = append(scpFlags, "-v")
-	} else {
-		scpFlags = append(scpFlags, "-q")
+	copyPaths, err := parseCopyArgs(ctx, args)
+	if err != nil {
+		return err
 	}
 
-	if recursive {
-		scpFlags = append(scpFlags, "-r")
+	backend, err := cmd.Flags().GetString("backend")
+	if err != nil {
+		return err
+	}
+
+	cpTool, toolPath, err := selectCopyTool(ctx, copyPaths, backend)
+	if err != nil {
+		return err
+	}
+
+	logrus.Debugf("using copy tool %q", toolPath)
+
+	var copyCmd *exec.Cmd
+	switch cpTool {
+	case scp:
+		copyCmd, err = scpCommand(ctx, toolPath, copyPaths, verbose, recursive)
+	case rsync:
+		copyCmd, err = rsyncCommand(ctx, toolPath, copyPaths, verbose, recursive)
+	default:
+		err = fmt.Errorf("invalid copy tool %q", cpTool)
 	}
-	// this assumes that ssh and scp come from the same place, but scp has no -V
-	sshExe, err := sshutil.NewSSHExe()
 	if err != nil {
 		return err
 	}
-	legacySSH := sshutil.DetectOpenSSHVersion(ctx, sshExe).LessThan(*semver.New("8.0.0"))
+
+	copyCmd.Stdin = cmd.InOrStdin()
+	copyCmd.Stdout = cmd.OutOrStdout()
+	copyCmd.Stderr = cmd.ErrOrStderr()
+	logrus.Debugf("executing %v (may take a long time)", copyCmd)
+
+	// TODO: use syscall.Exec directly (results in losing tty?)
+	return copyCmd.Run()
+}
+
+func parseCopyArgs(ctx context.Context, args []string) ([]*copyPath, error) {
+	var copyPaths []*copyPath
+
 	for _, arg := range args {
+		cp := &copyPath{}
+
 		if runtime.GOOS == "windows" {
 			if filepath.IsAbs(arg) {
+				var err error
 				arg, err = ioutilx.WindowsSubsystemPath(ctx, arg)
 				if err != nil {
-					return err
+					return nil, err
 				}
 			} else {
 				arg = filepath.ToSlash(arg)
 			}
 		}
-		path := strings.Split(arg, ":")
-		switch len(path) {
+
+		parts := strings.SplitN(arg, ":", 2)
+		switch len(parts) {
 		case 1:
-			scpArgs = append(scpArgs, arg)
+			cp.path = arg
+			cp.isRemote = false
 		case 2:
-			instName := path[0]
-			inst, err := store.Inspect(ctx, instName)
+			cp.instanceName = parts[0]
+			cp.path = parts[1]
+			cp.isRemote = true
+
+			inst, err := store.Inspect(ctx, cp.instanceName)
 			if err != nil {
 				if errors.Is(err, os.ErrNotExist) {
-					return fmt.Errorf("instance %q does not exist, run `limactl create %s` to create a new instance", instName, instName)
+					return nil, fmt.Errorf("instance %q does not exist, run `limactl create %s` to create a new instance", cp.instanceName, cp.instanceName)
 				}
-				return err
+				return nil, err
 			}
 			if inst.Status == limatype.StatusStopped {
-				return fmt.Errorf("instance %q is stopped, run `limactl start %s` to start the instance", instName, instName)
+				return nil, fmt.Errorf("instance %q is stopped, run `limactl start %s` to start the instance", cp.instanceName, cp.instanceName)
 			}
+			cp.instance = inst
+		default:
+			return nil, fmt.Errorf("path %q contains multiple colons", arg)
+		}
+
+		copyPaths = append(copyPaths, cp)
+	}
+
+	return copyPaths, nil
+}
+
+func selectCopyTool(ctx context.Context, copyPaths []*copyPath, backend string) (copyTool, string, error) {
+	switch copyTool(backend) {
+	case scp:
+		scpPath, err := exec.LookPath("scp")
+		if err != nil {
+			return "", "", fmt.Errorf("scp not found on host: %w", err)
+		}
+		return scp, scpPath, nil
+	case rsync:
+		rsyncPath, err := exec.LookPath("rsync")
+		if err != nil {
+			return "", "", fmt.Errorf("rsync not found on host: %w", err)
+		}
+		if !rsyncAvailableOnGuests(ctx, copyPaths) {
+			return "", "", errors.New("rsync not available on guest(s)")
+		}
+		return rsync, rsyncPath, nil
+	case auto:
+		if rsyncPath, err := exec.LookPath("rsync"); err == nil {
+			if rsyncAvailableOnGuests(ctx, copyPaths) {
+				return rsync, rsyncPath, nil
+			}
+			logrus.Debugf("rsync not available on guest(s), falling back to scp")
+		} else {
+			logrus.Debugf("rsync not found on host, falling back to scp: %v", err)
+		}
+
+		scpPath, err := exec.LookPath("scp")
+		if err != nil {
+			return "", "", fmt.Errorf("neither rsync nor scp found on host: %w", err)
+		}
+		return scp, scpPath, nil
+	default:
+		return "", "", fmt.Errorf("invalid backend %q, must be one of: scp, rsync, auto", backend)
+	}
+}
+
+func rsyncAvailableOnGuests(ctx context.Context, copyPaths []*copyPath) bool {
+	instances := make(map[string]*limatype.Instance)
+
+	for _, cp := range copyPaths {
+		if cp.isRemote {
+			instances[cp.instanceName] = cp.instance
+		}
+	}
+
+	for instName, inst := range instances {
+		if !checkRsyncOnGuest(ctx, inst) {
+			logrus.Debugf("rsync not available on instance %q", instName)
+			return false
+		}
+	}
+
+	return true
+}
+
+func checkRsyncOnGuest(ctx context.Context, inst *limatype.Instance) bool {
+	sshExe, err := sshutil.NewSSHExe()
+	if err != nil {
+		logrus.Debugf("failed to create SSH executable: %v", err)
+		return false
+	}
+	sshOpts, err := sshutil.SSHOpts(ctx, sshExe, inst.Dir, *inst.Config.User.Name, false, false, false, false)
+	if err != nil {
+		logrus.Debugf("failed to get SSH options for rsync check: %v", err)
+		return false
+	}
+
+	sshArgs := sshutil.SSHArgsFromOpts(sshOpts)
+	checkCmd := exec.CommandContext(ctx, "ssh")
+	checkCmd.Args = append(checkCmd.Args, sshArgs...)
+	checkCmd.Args = append(checkCmd.Args,
+		"-p", fmt.Sprintf("%d", inst.SSHLocalPort),
+		fmt.Sprintf("%s@127.0.0.1", *inst.Config.User.Name),
+		"command -v rsync >/dev/null 2>&1",
+	)
+
+	err = checkCmd.Run()
+	return err == nil
+}
+
+func scpCommand(ctx context.Context, command string, copyPaths []*copyPath, verbose, recursive bool) (*exec.Cmd, error) {
+	instances := make(map[string]*limatype.Instance)
+	scpFlags := []string{}
+	scpArgs := []string{}
+
+	if verbose {
+		scpFlags = append(scpFlags, "-v")
+	} else {
+		scpFlags = append(scpFlags, "-q")
+	}
+
+	if recursive {
+		scpFlags = append(scpFlags, "-r")
+	}
+
+	// this assumes that ssh and scp come from the same place, but scp has no -V
+	sshExeForVersion, err := sshutil.NewSSHExe()
+	if err != nil {
+		return nil, err
+	}
+	legacySSH := sshutil.DetectOpenSSHVersion(ctx, sshExeForVersion).LessThan(*semver.New("8.0.0"))
+
+	for _, cp := range copyPaths {
+		if cp.isRemote {
 			if legacySSH {
-				scpFlags = append(scpFlags, "-P", fmt.Sprintf("%d", inst.SSHLocalPort))
-				scpArgs = append(scpArgs, fmt.Sprintf("%s@127.0.0.1:%s", *inst.Config.User.Name, path[1]))
+				scpFlags = append(scpFlags, "-P", fmt.Sprintf("%d", cp.instance.SSHLocalPort))
+				scpArgs = append(scpArgs, fmt.Sprintf("%s@127.0.0.1:%s", *cp.instance.Config.User.Name, cp.path))
 			} else {
-				scpArgs = append(scpArgs, fmt.Sprintf("scp://%s@127.0.0.1:%d/%s", *inst.Config.User.Name, inst.SSHLocalPort, path[1]))
+				scpArgs = append(scpArgs, fmt.Sprintf("scp://%s@127.0.0.1:%d/%s", *cp.instance.Config.User.Name, cp.instance.SSHLocalPort, cp.path))
 			}
-			instances[instName] = inst
-		default:
-			return fmt.Errorf("path %q contains multiple colons", arg)
+			instances[cp.instanceName] = cp.instance
+		} else {
+			scpArgs = append(scpArgs, cp.path)
 		}
 	}
+
 	if legacySSH && len(instances) > 1 {
-		return errors.New("more than one (instance) host is involved in this command, this is only supported for openSSH v8.0 or higher")
+		return nil, errors.New("more than one (instance) host is involved in this command, this is only supported for openSSH v8.0 or higher")
 	}
+
 	scpFlags = append(scpFlags, "-3", "--")
 	scpArgs = append(scpFlags, scpArgs...)
 
@@ -143,32 +325,78 @@ func copyAction(cmd *cobra.Command, args []string) error {
 		for _, inst := range instances {
 			sshExe, err := sshutil.NewSSHExe()
 			if err != nil {
-				return err
+				return nil, err
 			}
 			sshOpts, err = sshutil.SSHOpts(ctx, sshExe, inst.Dir, *inst.Config.User.Name, false, false, false, false)
 			if err != nil {
-				return err
+				return nil, err
 			}
 		}
 	} else {
 		// Copying among multiple hosts; we can't pass in host-specific options.
 		sshExe, err := sshutil.NewSSHExe()
 		if err != nil {
-			return err
+			return nil, err
 		}
 		sshOpts, err = sshutil.CommonOpts(ctx, sshExe, false)
 		if err != nil {
-			return err
+			return nil, err
 		}
 	}
 	sshArgs := sshutil.SSHArgsFromOpts(sshOpts)
 
-	sshCmd := exec.CommandContext(ctx, arg0, append(sshArgs, scpArgs...)...)
-	sshCmd.Stdin = cmd.InOrStdin()
-	sshCmd.Stdout = cmd.OutOrStdout()
-	sshCmd.Stderr = cmd.ErrOrStderr()
-	logrus.Debugf("executing scp (may take a long time): %+v", sshCmd.Args)
+	return exec.CommandContext(ctx, command, append(sshArgs, scpArgs...)...), nil
+}
 
-	// TODO: use syscall.Exec directly (results in losing tty?)
-	return sshCmd.Run()
+func rsyncCommand(ctx context.Context, command string, copyPaths []*copyPath, verbose, recursive bool) (*exec.Cmd, error) {
+	rsyncFlags := []string{"-a"}
+
+	if verbose {
+		rsyncFlags = append(rsyncFlags, "-v", "--progress")
+	} else {
+		rsyncFlags = append(rsyncFlags, "-q")
+	}
+
+	if recursive {
+		rsyncFlags = append(rsyncFlags, "-r")
+	}
+
+	rsyncArgs := make([]string, 0, len(rsyncFlags)+len(copyPaths))
+	rsyncArgs = append(rsyncArgs, rsyncFlags...)
+
+	var sshCmd string
+	var remoteInstance *limatype.Instance
+
+	for _, cp := range copyPaths {
+		if cp.isRemote {
+			if remoteInstance == nil {
+				remoteInstance = cp.instance
+				sshExe, err := sshutil.NewSSHExe()
+				if err != nil {
+					return nil, err
+				}
+				sshOpts, err := sshutil.SSHOpts(ctx, sshExe, cp.instance.Dir, *cp.instance.Config.User.Name, false, false, false, false)
+				if err != nil {
+					return nil, err
+				}
+
+				sshArgs := sshutil.SSHArgsFromOpts(sshOpts)
+				sshCmd = fmt.Sprintf("ssh -p %d %s", cp.instance.SSHLocalPort, strings.Join(sshArgs, " "))
+			}
+		}
+	}
+
+	if sshCmd != "" {
+		rsyncArgs = append(rsyncArgs, "-e", sshCmd)
+	}
+
+	for _, cp := range copyPaths {
+		if cp.isRemote {
+			rsyncArgs = append(rsyncArgs, fmt.Sprintf("%s@127.0.0.1:%s", *cp.instance.Config.User.Name, cp.path))
+		} else {
+			rsyncArgs = append(rsyncArgs, cp.path)
+		}
+	}
+
+	return exec.CommandContext(ctx, command, rsyncArgs...), nil
 }
diff --git a/cmd/limactl/editflags/editflags.go b/cmd/limactl/editflags/editflags.go
index 6b8d21d6..7047a79c 100644
--- a/cmd/limactl/editflags/editflags.go
+++ b/cmd/limactl/editflags/editflags.go
@@ -8,6 +8,7 @@ import (
 	"fmt"
 	"math/bits"
 	"runtime"
+	"slices"
 	"strconv"
 	"strings"
 
@@ -179,7 +180,7 @@ func buildMountListExpression(ss []string) (string, error) {
 		if err != nil {
 			return "", err
 		}
-		expr += fmt.Sprintf(`{"location": %q, "writable": %v}`, loc, writable)
+		expr += fmt.Sprintf(`{"location": %q, "mountPoint": %q, "writable": %v}`, loc, loc, writable)
 		if i < len(ss)-1 {
 			expr += ","
 		}
@@ -225,6 +226,7 @@ func YQExpressions(flags *flag.FlagSet, newInstance bool) ([]string, error) {
 			"mount",
 			func(_ *flag.Flag) (string, error) {
 				ss, err := flags.GetStringSlice("mount")
+				slices.Reverse(ss)
 				if err != nil {
 					return "", err
 				}
@@ -232,7 +234,8 @@ func YQExpressions(flags *flag.FlagSet, newInstance bool) ([]string, error) {
 				if err != nil {
 					return "", err
 				}
-				expr := `.mounts += ` + mountListExpr + ` | .mounts |= unique_by(.location)`
+				// mount options take precedence over template settings
+				expr := fmt.Sprintf(".mounts = %s + .mounts", mountListExpr)
 				mountOnly, err := flags.GetStringSlice("mount-only")
 				if err != nil {
 					return "", err
diff --git a/cmd/limactl/hostagent.go b/cmd/limactl/hostagent.go
index d0a363f7..0ad654ed 100644
--- a/cmd/limactl/hostagent.go
+++ b/cmd/limactl/hostagent.go
@@ -20,6 +20,7 @@ import (
 
 	"github.com/lima-vm/lima/v2/pkg/hostagent"
 	"github.com/lima-vm/lima/v2/pkg/hostagent/api/server"
+	"github.com/lima-vm/lima/v2/pkg/store"
 )
 
 func newHostagentCommand() *cobra.Command {
@@ -46,8 +47,10 @@ func hostagentAction(cmd *cobra.Command, args []string) error {
 		return err
 	}
 	if pidfile != "" {
-		if _, err := os.Stat(pidfile); !errors.Is(err, os.ErrNotExist) {
-			return fmt.Errorf("pidfile %q already exists", pidfile)
+		if existingPID, err := store.ReadPIDFile(pidfile); existingPID != 0 {
+			return fmt.Errorf("another hostagent may already be running with pid %d (pidfile %q)", existingPID, pidfile)
+		} else if err != nil {
+			return fmt.Errorf("failed to determine if another hostagent is running: %w", err)
 		}
 		if err := os.WriteFile(pidfile, []byte(strconv.Itoa(os.Getpid())+"\n"), 0o644); err != nil {
 			return err
diff --git a/cmd/limactl/usernet.go b/cmd/limactl/usernet.go
index 37777de6..9c11ce39 100644
--- a/cmd/limactl/usernet.go
+++ b/cmd/limactl/usernet.go
@@ -7,7 +7,9 @@ import (
 	"errors"
 	"fmt"
 	"os"
+	"os/signal"
 	"strconv"
+	"syscall"
 
 	"github.com/spf13/cobra"
 
@@ -77,10 +79,13 @@ func usernetAction(cmd *cobra.Command, _ []string) error {
 	os.RemoveAll(qemuSocket)
 	os.RemoveAll(fdSocket)
 
+	ctx, cancel := signal.NotifyContext(cmd.Context(), os.Interrupt, syscall.SIGTERM)
+	defer cancel()
+
 	// Environment Variables
 	// LIMA_USERNET_RESOLVE_IP_ADDRESS_TIMEOUT: Specifies the timeout duration for resolving IP addresses in minutes. Default is 2 minutes.
 
-	return usernet.StartGVisorNetstack(cmd.Context(), &usernet.GVisorNetstackOpts{
+	return usernet.StartGVisorNetstack(ctx, &usernet.GVisorNetstackOpts{
 		MTU:           mtu,
 		Endpoint:      endpoint,
 		QemuSocket:    qemuSocket,
diff --git a/hack/test-templates.sh b/hack/test-templates.sh
index 7faa5f2a..3322a780 100755
--- a/hack/test-templates.sh
+++ b/hack/test-templates.sh
@@ -260,6 +260,57 @@ if [ "$got" != "$expected" ]; then
 	exit 1
 fi
 
+INFO "Testing limactl copy command with scp backend"
+tmpfile_scp="$tmpdir/lima-hostname-scp"
+rm -f "$tmpfile_scp"
+tmpfile_scp_host=$tmpfile_scp
+if [ "${OS_HOST}" = "Msys" ]; then
+	tmpfile_scp_host="$(cygpath -w "$tmpfile_scp")"
+fi
+limactl cp --backend=scp "$NAME":/etc/hostname "$tmpfile_scp_host"
+expected="$(limactl shell "$NAME" cat /etc/hostname)"
+got="$(cat "$tmpfile_scp")"
+INFO "/etc/hostname (scp): expected=${expected}, got=${got}"
+if [ "$got" != "$expected" ]; then
+	ERROR "copy command with scp backend did not fetch the file"
+	exit 1
+fi
+
+if command -v rsync >/dev/null && limactl shell "$NAME" command -v rsync >/dev/null 2>&1; then
+	INFO "Testing limactl copy command with rsync backend"
+	tmpfile_rsync="$tmpdir/lima-hostname-rsync"
+	rm -f "$tmpfile_rsync"
+	tmpfile_rsync_host=$tmpfile_rsync
+	if [ "${OS_HOST}" = "Msys" ]; then
+		tmpfile_rsync_host="$(cygpath -w "$tmpfile_rsync")"
+	fi
+	limactl cp --backend=rsync "$NAME":/etc/hostname "$tmpfile_rsync_host"
+	expected="$(limactl shell "$NAME" cat /etc/hostname)"
+	got="$(cat "$tmpfile_rsync")"
+	INFO "/etc/hostname (rsync): expected=${expected}, got=${got}"
+	if [ "$got" != "$expected" ]; then
+		ERROR "copy command with rsync backend did not fetch the file"
+		exit 1
+	fi
+
+	INFO "Testing limactl copy command with rsync backend (verbose, recursive)"
+	testdir="$tmpdir/test-rsync-dir"
+	mkdir -p "$testdir"
+	echo "test content" >"$testdir/testfile.txt"
+	limactl cp --backend=rsync -r -v "$testdir" "$NAME":/tmp/
+	if ! limactl shell "$NAME" test -f /tmp/test-rsync-dir/testfile.txt; then
+		ERROR "rsync recursive copy failed"
+		exit 1
+	fi
+	rsync_content="$(limactl shell "$NAME" cat /tmp/test-rsync-dir/testfile.txt)"
+	if [ "$rsync_content" != "test content" ]; then
+		ERROR "rsync file content mismatch"
+		exit 1
+	fi
+else
+	INFO "Skipping rsync backend test (rsync not available on host or guest)"
+fi
+
 INFO "Testing limactl command with escaped characters"
 limactl shell "$NAME" bash -c "$(echo -e '\n\techo foo\n\techo bar')"
 
diff --git a/pkg/cidata/cidata.TEMPLATE.d/boot/30-install-packages.sh b/pkg/cidata/cidata.TEMPLATE.d/boot/30-install-packages.sh
index e32a6992..a274b0a0 100644
--- a/pkg/cidata/cidata.TEMPLATE.d/boot/30-install-packages.sh
+++ b/pkg/cidata/cidata.TEMPLATE.d/boot/30-install-packages.sh
@@ -53,6 +53,9 @@ if head -c 4 "$(command -v apt-get)" | grep -qP '\x7fELF' >/dev/null 2>&1; then
 	if [ "${LIMA_CIDATA_CONTAINERD_USER}" = 1 ] && ! command -v newuidmap >/dev/null 2>&1; then
 		pkgs="${pkgs} uidmap fuse3 dbus-user-session"
 	fi
+	if ! command -v rsync >/dev/null 2>&1; then
+		pkgs="${pkgs} rsync"
+	fi
 	if [ -n "${pkgs}" ]; then
 		DEBIAN_FRONTEND=noninteractive
 		export DEBIAN_FRONTEND
@@ -83,6 +86,9 @@ elif command -v dnf >/dev/null 2>&1; then
 			pkgs="${pkgs} fuse3"
 		fi
 	fi
+	if ! command -v rsync >/dev/null 2>&1; then
+		pkgs="${pkgs} rsync"
+	fi
 	if [ -n "${pkgs}" ] || [ -n "${extrapkgs}" ]; then
 		dnf_install_flags="-y --setopt=install_weak_deps=False"
 		epel_install_flags=""
@@ -148,6 +154,9 @@ elif command -v yum >/dev/null 2>&1; then
 			pkgs="${pkgs} fuse3"
 		fi
 	fi
+	if ! command -v rsync >/dev/null 2>&1; then
+		pkgs="${pkgs} rsync"
+	fi
 	if [ -n "${pkgs}" ]; then
 		# shellcheck disable=SC2086
 		yum install ${yum_install_flags} ${pkgs}
@@ -160,6 +169,9 @@ elif command -v pacman >/dev/null 2>&1; then
 			pkgs="${pkgs} sshfs"
 		fi
 	fi
+	if ! command -v rsync >/dev/null 2>&1; then
+		pkgs="${pkgs} rsync"
+	fi
 	# other dependencies are preinstalled on Arch Linux
 	if [ -n "${pkgs}" ]; then
 		# shellcheck disable=SC2086
@@ -178,6 +190,9 @@ elif command -v zypper >/dev/null 2>&1; then
 	if [ "${LIMA_CIDATA_CONTAINERD_USER}" = 1 ] && ! command -v mount.fuse3 >/dev/null 2>&1; then
 		pkgs="${pkgs} fuse3"
 	fi
+	if ! command -v rsync >/dev/null 2>&1; then
+		pkgs="${pkgs} rsync"
+	fi
 	if [ -n "${pkgs}" ]; then
 		# shellcheck disable=SC2086
 		zypper --non-interactive install -y --no-recommends ${pkgs}
@@ -192,6 +207,9 @@ elif command -v apk >/dev/null 2>&1; then
 	if [ "${INSTALL_IPTABLES}" = 1 ] && ! command -v iptables >/dev/null 2>&1; then
 		pkgs="${pkgs} iptables"
 	fi
+	if ! command -v rsync >/dev/null 2>&1; then
+		pkgs="${pkgs} rsync"
+	fi
 	if [ -n "${pkgs}" ]; then
 		apk update
 		# shellcheck disable=SC2086
diff --git a/pkg/cidata/cidata.TEMPLATE.d/lima.env b/pkg/cidata/cidata.TEMPLATE.d/lima.env
index e08129b5..8cd9453c 100644
--- a/pkg/cidata/cidata.TEMPLATE.d/lima.env
+++ b/pkg/cidata/cidata.TEMPLATE.d/lima.env
@@ -65,3 +65,8 @@ LIMA_CIDATA_PLAIN=1
 {{- else}}
 LIMA_CIDATA_PLAIN=
 {{- end}}
+{{- if .NoCloudInit}}
+LIMA_CIDATA_NO_CLOUD_INIT=1
+{{- else}}
+LIMA_CIDATA_NO_CLOUD_INIT=
+{{- end}}
diff --git a/pkg/cidata/cidata.go b/pkg/cidata/cidata.go
index 4a38c0c2..ad024acb 100644
--- a/pkg/cidata/cidata.go
+++ b/pkg/cidata/cidata.go
@@ -118,7 +118,7 @@ func setupEnv(instConfigEnv map[string]string, propagateProxyEnv bool, slirpGate
 	return env, nil
 }
 
-func templateArgs(ctx context.Context, bootScripts bool, instDir, name string, instConfig *limatype.LimaYAML, udpDNSLocalPort, tcpDNSLocalPort, vsockPort int, virtioPort string) (*TemplateArgs, error) {
+func templateArgs(ctx context.Context, bootScripts bool, instDir, name string, instConfig *limatype.LimaYAML, udpDNSLocalPort, tcpDNSLocalPort, vsockPort int, virtioPort string, noCloudInit bool) (*TemplateArgs, error) {
 	if err := limayaml.Validate(instConfig, false); err != nil {
 		return nil, err
 	}
@@ -138,12 +138,13 @@ func templateArgs(ctx context.Context, bootScripts bool, instDir, name string, i
 		Containerd:         Containerd{System: *instConfig.Containerd.System, User: *instConfig.Containerd.User, Archive: archive},
 		SlirpNICName:       networks.SlirpNICName,
 
-		VMType:     *instConfig.VMType,
-		VSockPort:  vsockPort,
-		VirtioPort: virtioPort,
-		Plain:      *instConfig.Plain,
-		TimeZone:   *instConfig.TimeZone,
-		Param:      instConfig.Param,
+		VMType:      *instConfig.VMType,
+		VSockPort:   vsockPort,
+		VirtioPort:  virtioPort,
+		Plain:       *instConfig.Plain,
+		TimeZone:    *instConfig.TimeZone,
+		NoCloudInit: noCloudInit,
+		Param:       instConfig.Param,
 	}
 
 	if instConfig.VMOpts.VZ.Rosetta.Enabled != nil {
@@ -350,7 +351,7 @@ func templateArgs(ctx context.Context, bootScripts bool, instDir, name string, i
 }
 
 func GenerateCloudConfig(ctx context.Context, instDir, name string, instConfig *limatype.LimaYAML) error {
-	args, err := templateArgs(ctx, false, instDir, name, instConfig, 0, 0, 0, "")
+	args, err := templateArgs(ctx, false, instDir, name, instConfig, 0, 0, 0, "", false)
 	if err != nil {
 		return err
 	}
@@ -372,8 +373,8 @@ func GenerateCloudConfig(ctx context.Context, instDir, name string, instConfig *
 	return os.WriteFile(filepath.Join(instDir, filenames.CloudConfig), config, 0o444)
 }
 
-func GenerateISO9660(ctx context.Context, drv driver.Driver, instDir, name string, instConfig *limatype.LimaYAML, udpDNSLocalPort, tcpDNSLocalPort int, guestAgentBinary, nerdctlArchive string, vsockPort int, virtioPort string) error {
-	args, err := templateArgs(ctx, true, instDir, name, instConfig, udpDNSLocalPort, tcpDNSLocalPort, vsockPort, virtioPort)
+func GenerateISO9660(ctx context.Context, drv driver.Driver, instDir, name string, instConfig *limatype.LimaYAML, udpDNSLocalPort, tcpDNSLocalPort int, guestAgentBinary, nerdctlArchive string, vsockPort int, virtioPort string, noCloudInit bool) error {
+	args, err := templateArgs(ctx, true, instDir, name, instConfig, udpDNSLocalPort, tcpDNSLocalPort, vsockPort, virtioPort, noCloudInit)
 	if err != nil {
 		return err
 	}
@@ -466,7 +467,7 @@ func GenerateISO9660(ctx context.Context, drv driver.Driver, instDir, name strin
 		})
 	}
 
-	if args.VMType == limatype.WSL2 {
+	if noCloudInit {
 		layout = append(layout, iso9660util.Entry{
 			Path:   "ssh_authorized_keys",
 			Reader: strings.NewReader(strings.Join(args.SSHPubKeys, "\n")),
diff --git a/pkg/cidata/template.go b/pkg/cidata/template.go
index 3d886e7a..84dfafce 100644
--- a/pkg/cidata/template.go
+++ b/pkg/cidata/template.go
@@ -114,6 +114,7 @@ type TemplateArgs struct {
 	VirtioPort                      string
 	Plain                           bool
 	TimeZone                        string
+	NoCloudInit                     bool
 }
 
 func ValidateTemplateArgs(args *TemplateArgs) error {
diff --git a/pkg/driver/driver.go b/pkg/driver/driver.go
index 94c192f0..fc3a1afd 100644
--- a/pkg/driver/driver.go
+++ b/pkg/driver/driver.go
@@ -106,4 +106,5 @@ type DriverFeatures struct {
 	CanRunGUI            bool `json:"canRunGui,omitempty"`
 	DynamicSSHAddress    bool `json:"dynamicSSHAddress"`
 	SkipSocketForwarding bool `json:"skipSocketForwarding"`
+	NoCloudInit          bool `json:"noCloudInit"`
 }
diff --git a/pkg/driver/qemu/qemu.go b/pkg/driver/qemu/qemu.go
index 858dad02..1af03ae0 100644
--- a/pkg/driver/qemu/qemu.go
+++ b/pkg/driver/qemu/qemu.go
@@ -935,7 +935,7 @@ func Cmdline(ctx context.Context, cfg Config) (exe string, args []string, err er
 				options += fmt.Sprintf(",path=%s", f.Location)
 				options += fmt.Sprintf(",security_model=%s", *f.NineP.SecurityModel)
 				if !*f.Writable {
-					options += ",readonly"
+					options += ",readonly=on"
 				}
 				args = append(args, "-virtfs", options)
 			case limatype.VIRTIOFS:
diff --git a/pkg/driver/wsl2/boot/02-wsl2-setup.sh b/pkg/driver/wsl2/boot/02-no-cloud-init-setup.sh
similarity index 85%
rename from pkg/driver/wsl2/boot/02-wsl2-setup.sh
rename to pkg/driver/wsl2/boot/02-no-cloud-init-setup.sh
index cb563965..ebfe351c 100755
--- a/pkg/driver/wsl2/boot/02-wsl2-setup.sh
+++ b/pkg/driver/wsl2/boot/02-no-cloud-init-setup.sh
@@ -4,8 +4,8 @@
 # SPDX-License-Identifier: Apache-2.0
 
 # This script replaces the cloud-init functionality of creating a user and setting its SSH keys
-# when using a WSL2 VM.
-[ "$LIMA_CIDATA_VMTYPE" = "wsl2" ] || exit 0
+# when cloud-init is not available
+[ "$LIMA_CIDATA_NO_CLOUD_INIT" = "1" ] || exit 0
 
 # create user
 # shellcheck disable=SC2153
@@ -22,4 +22,4 @@ chmod 600 "${LIMA_CIDATA_HOME}"/.ssh/authorized_keys
 echo "${LIMA_CIDATA_USER} ALL=(ALL) NOPASSWD:ALL" | tee -a /etc/sudoers.d/99_lima_sudoers
 
 # symlink CIDATA to the hardcoded path for requirement checks (TODO: make this not hardcoded)
-ln -sfFn "${LIMA_CIDATA_MNT}" /mnt/lima-cidata
+[ "$LIMA_CIDATA_MNT" = "/mnt/lima-cidata" ] || ln -sfFn "${LIMA_CIDATA_MNT}" /mnt/lima-cidata
diff --git a/pkg/driver/wsl2/wsl_driver_windows.go b/pkg/driver/wsl2/wsl_driver_windows.go
index 17886de8..ae8252b1 100644
--- a/pkg/driver/wsl2/wsl_driver_windows.go
+++ b/pkg/driver/wsl2/wsl_driver_windows.go
@@ -310,6 +310,7 @@ func (l *LimaWslDriver) Info() driver.Info {
 	info.Features = driver.DriverFeatures{
 		DynamicSSHAddress:    true,
 		SkipSocketForwarding: true,
+		NoCloudInit:          true,
 		CanRunGUI:            l.canRunGUI(),
 	}
 	return info
diff --git a/pkg/hostagent/hostagent.go b/pkg/hostagent/hostagent.go
index 3affdf6b..41738e2d 100644
--- a/pkg/hostagent/hostagent.go
+++ b/pkg/hostagent/hostagent.go
@@ -162,11 +162,12 @@ func New(ctx context.Context, instName string, stdout io.Writer, signalCh chan o
 
 	vSockPort := limaDriver.Info().VsockPort
 	virtioPort := limaDriver.Info().VirtioPort
+	noCloudInit := limaDriver.Info().Features.NoCloudInit
 
 	if err := cidata.GenerateCloudConfig(ctx, inst.Dir, instName, inst.Config); err != nil {
 		return nil, err
 	}
-	if err := cidata.GenerateISO9660(ctx, limaDriver, inst.Dir, instName, inst.Config, udpDNSLocalPort, tcpDNSLocalPort, o.guestAgentBinary, o.nerdctlArchive, vSockPort, virtioPort); err != nil {
+	if err := cidata.GenerateISO9660(ctx, limaDriver, inst.Dir, instName, inst.Config, udpDNSLocalPort, tcpDNSLocalPort, o.guestAgentBinary, o.nerdctlArchive, vSockPort, virtioPort, noCloudInit); err != nil {
 		return nil, err
 	}
 
diff --git a/pkg/networks/usernet/gvproxy.go b/pkg/networks/usernet/gvproxy.go
index 4df2c62d..363ac996 100644
--- a/pkg/networks/usernet/gvproxy.go
+++ b/pkg/networks/usernet/gvproxy.go
@@ -6,6 +6,7 @@ package usernet
 import (
 	"bufio"
 	"context"
+	"errors"
 	"fmt"
 	"net"
 	"net/http"
@@ -128,9 +129,16 @@ func listenQEMU(ctx context.Context, vn *virtualnetwork.VirtualNetwork) error {
 
 	go func() {
 		defer listener.Close()
+		<-ctx.Done()
+	}()
+
+	go func() {
 		for {
 			conn, err := listener.Accept()
 			if err != nil {
+				if errors.Is(err, net.ErrClosed) {
+					return
+				}
 				logrus.Error("QEMU accept failed", err)
 			}
 
@@ -162,10 +170,18 @@ func listenFD(ctx context.Context, vn *virtualnetwork.VirtualNetwork) error {
 
 	go func() {
 		defer listener.Close()
+		<-ctx.Done()
+	}()
+
+	go func() {
 		for {
 			conn, err := listener.Accept()
 			if err != nil {
+				if errors.Is(err, net.ErrClosed) {
+					return
+				}
 				logrus.Error("FD accept failed", err)
+				continue // since conn is nil
 			}
 
 			files, err := fd.Get(conn.(*net.UnixConn), 1, []string{"client"})
@@ -202,20 +218,20 @@ func listenFD(ctx context.Context, vn *virtualnetwork.VirtualNetwork) error {
 }
 
 func httpServe(ctx context.Context, g *errgroup.Group, ln net.Listener, mux http.Handler) {
+	s := &http.Server{
+		Handler:      mux,
+		ReadTimeout:  10 * time.Second,
+		WriteTimeout: 10 * time.Second,
+	}
 	g.Go(func() error {
 		<-ctx.Done()
-		return ln.Close()
+		return s.Close()
 	})
 	g.Go(func() error {
-		s := &http.Server{
-			Handler:      mux,
-			ReadTimeout:  10 * time.Second,
-			WriteTimeout: 10 * time.Second,
-		}
 		err := s.Serve(ln)
 		if err != nil {
-			if err != http.ErrServerClosed {
-				return err
+			if err == http.ErrServerClosed {
+				return nil
 			}
 			return err
 		}
diff --git a/pkg/networks/usernet/udpfileconn.go b/pkg/networks/usernet/udpfileconn.go
index ef04b841..ea8696c0 100644
--- a/pkg/networks/usernet/udpfileconn.go
+++ b/pkg/networks/usernet/udpfileconn.go
@@ -16,8 +16,7 @@ type UDPFileConn struct {
 func (conn *UDPFileConn) Read(b []byte) (n int, err error) {
 	// Check if the connection has been closed
 	if err := conn.SetReadDeadline(time.Time{}); err != nil {
-		var opErr *net.OpError
-		if errors.As(err, &opErr) && opErr.Err.Error() == "use of closed network connection" {
+		if errors.Is(err, net.ErrClosed) {
 			return 0, errors.New("UDPFileConn connection closed")
 		}
 	}



Test patch:
diff --git a/cmd/limactl/editflags/editflags_test.go b/cmd/limactl/editflags/editflags_test.go
index ae9e178e..c580bb0d 100644
--- a/cmd/limactl/editflags/editflags_test.go
+++ b/cmd/limactl/editflags/editflags_test.go
@@ -181,13 +181,13 @@ func TestYQExpressions(t *testing.T) {
 			name:        "mount",
 			args:        []string{"--mount", "/foo", "--mount", "./bar:w"},
 			newInstance: false,
-			expected:    []string{`.mounts += [{"location": "` + expand("/foo") + `", "writable": false},{"location": "` + expand("./bar") + `", "writable": true}] | .mounts |= unique_by(.location)`},
+			expected:    []string{`.mounts = [{"location": "` + expand("./bar") + `", "mountPoint": "` + expand("./bar") + `", "writable": true},{"location": "` + expand("/foo") + `", "mountPoint": "` + expand("/foo") + `", "writable": false}] + .mounts`},
 		},
 		{
 			name:        "mount-only",
 			args:        []string{"--mount-only", "/foo", "--mount-only", "/bar:w"},
 			newInstance: false,
-			expected:    []string{`.mounts = [{"location": "` + expand("/foo") + `", "writable": false},{"location": "` + expand("/bar") + `", "writable": true}]`},
+			expected:    []string{`.mounts = [{"location": "` + expand("/foo") + `", "mountPoint": "` + expand("/foo") + `", "writable": false},{"location": "` + expand("/bar") + `", "mountPoint": "` + expand("/bar") + `", "writable": true}]`},
 		},
 		{
 			name:        "mixture of mount and mount-only",