From 28de2415ba4f8e87518d56bb0343267405aed97e Mon Sep 17 00:00:00 2001 From: Norio Nomura Date: Mon, 10 Nov 2025 19:15:05 +0900 Subject: [PATCH 1/6] pkg/driver/vz: Support ASIF as diffdisk Depends on https://github.com/lima-vm/go-qcow2reader/pull/61 How to setup ASIF as diffdisk: 1. Create an instance for test, then stop it. ```console $ limactl start template:ubuntu --name=asif-test --tty=false --log-level=fatal; limactl stop asif-test --log-level=fatal ``` 2. Convert `diffdisk` with ASIF image. (original will be renamed to `diffdisk.raw`) ```console $ hack/convert-diffdisk-to-asif.sh asif-test + instance=asif-test ++ limactl list asif-test --format '{{.Dir}}' + instance_dir=/Users/norio/.lima/asif-test ++ head -c 4 /Users/norio/.lima/asif-test/diffdisk + head4bytes= + case "${head4bytes}" in ++ limactl list asif-test --format '{{.Status}}' + instance_state=Stopped + [[ Stopped == \S\t\o\p\p\e\d ]] + diskutil image create blank --fs none --format ASIF --size 100GiB /Users/norio/.lima/asif-test/diffdisk.asif /Users/norio/.lima/asif-test/diffdisk.asif created ++ diskutil image attach -n /Users/norio/.lima/asif-test/diffdisk.asif + attached_device=/dev/disk5 + dd if=/Users/norio/.lima/asif-test/diffdisk of=/dev/disk5 status=progress conv=sparse 107152496640 bytes (107 GB, 100 GiB) transferred 115.003s, 932 MB/s 209715200+0 records in 209715200+0 records out 107374182400 bytes transferred in 115.228413 secs (931837727 bytes/sec) + hdiutil detach /dev/disk5 "disk5" ejected. + mv /Users/norio/.lima/asif-test/diffdisk /Users/norio/.lima/asif-test/diffdisk.raw + mv /Users/norio/.lima/asif-test/diffdisk.asif /Users/norio/.lima/asif-test/diffdisk + echo 'Converted diffdisk to ASIF format successfully' Converted diffdisk to ASIF format successfully ``` 3. Start the instance ```console $ limactl start asif-test ``` Signed-off-by: Norio Nomura Apply reviews - hack/convert-diffdisk-to-asif.sh: Add description of script. - pkg/driver/vz: Refine error text on detecting unexpected image type. Signed-off-by: Norio Nomura --- hack/convert-diffdisk-to-asif.sh | 60 ++++++++++++++++++++++++++++++++ pkg/driver/vz/vm_darwin.go | 8 +++-- 2 files changed, 66 insertions(+), 2 deletions(-) create mode 100755 hack/convert-diffdisk-to-asif.sh diff --git a/hack/convert-diffdisk-to-asif.sh b/hack/convert-diffdisk-to-asif.sh new file mode 100755 index 00000000000..b5088e213fe --- /dev/null +++ b/hack/convert-diffdisk-to-asif.sh @@ -0,0 +1,60 @@ +#!/bin/bash + +# SPDX-FileCopyrightText: Copyright The Lima Authors +# SPDX-License-Identifier: Apache-2.0 + +# This script converts the diffdisk of a Lima instance to ASIF format. +# It requires that the instance is stopped before conversion. +# Usage: hack/convert-diffdisk-to-asif.sh [instance-name] + +set -eux -o pipefail + +instance="${1:-asif-test}" + +# Get instance dir +instance_dir=$(limactl list "${instance}" --format "{{.Dir}}") || { + echo "Failed to get instance dir for ${instance}" + exit 1 +} + +# Check diffdisk type +head4bytes="$(head -c 4 "${instance_dir}/diffdisk")" +case "${head4bytes}" in +shdw) + echo "diffdisk is already in ASIF format" + exit 1 + ;; +QFI*) + echo "diffdisk is in QCOW2 format" + exit 1 + ;; +*) ;; +esac + +# Check instance state +instance_state="$(limactl list "${instance}" --format "{{.Status}}")" || { + echo "Failed to get instance state for ${instance}" + exit 1 +} +[[ ${instance_state} == "Stopped" ]] || { + echo "Instance ${instance} must be stopped" + exit 1 +} + +# Create ASIF image +diskutil image create blank --fs none --format ASIF --size 100GiB "${instance_dir}/diffdisk.asif" + +# Attach ASIF image (`hdiutil attach` does not support attaching ASIF) +attached_device=$(diskutil image attach -n "${instance_dir}/diffdisk.asif") + +# Write `diffdisk` content to attached device using `dd` with `conv=sparse` option (`diskutil` does not support sparse) +dd if="${instance_dir}/diffdisk" of="${attached_device}" status=progress conv=sparse + +# Detach the device (`diskutil unmountDisk` does not detach the device) +hdiutil detach "${attached_device}" + +# Replace `diffdisk` with `diffdisk.asif` +mv "${instance_dir}/diffdisk" "${instance_dir}/diffdisk.raw" +mv "${instance_dir}/diffdisk.asif" "${instance_dir}/diffdisk" + +echo "Converted diffdisk to ASIF format successfully" diff --git a/pkg/driver/vz/vm_darwin.go b/pkg/driver/vz/vm_darwin.go index 15cff0444e3..cbce17b6a5a 100644 --- a/pkg/driver/vz/vm_darwin.go +++ b/pkg/driver/vz/vm_darwin.go @@ -14,6 +14,7 @@ import ( "os" "path/filepath" "runtime" + "slices" "strconv" "sync" "syscall" @@ -22,6 +23,8 @@ import ( "github.com/coreos/go-semver/semver" "github.com/docker/go-units" "github.com/lima-vm/go-qcow2reader" + "github.com/lima-vm/go-qcow2reader/image" + "github.com/lima-vm/go-qcow2reader/image/asif" "github.com/lima-vm/go-qcow2reader/image/raw" "github.com/sirupsen/logrus" @@ -451,8 +454,9 @@ func validateDiskFormat(diskPath string) error { if err != nil { return fmt.Errorf("failed to detect the format of %q: %w", diskPath, err) } - if t := img.Type(); t != raw.Type { - return fmt.Errorf("expected the format of %q to be %q, got %q", diskPath, raw.Type, t) + supportedDiskTypes := []image.Type{raw.Type, asif.Type} + if t := img.Type(); !slices.Contains(supportedDiskTypes, t) { + return fmt.Errorf("expected the format of %q to be one of %v, got %q", diskPath, supportedDiskTypes, t) } // TODO: ensure that the disk is formatted with GPT or ISO9660 return nil From 3aa190e25d6f63734b080ebf0d347255d42c6451 Mon Sep 17 00:00:00 2001 From: Norio Nomura Date: Tue, 11 Nov 2025 19:24:52 +0900 Subject: [PATCH 2/6] vz: Support ASIF on creating diffdisk - Add `LIMA_VZ_ASIF` environment variable to use ASIF on creating diffdisk - pkg/imgutil: Add `ImageDiskManager.ConvertToASIF()` - Remove `hack/convert-diffdisk-to-asif.sh` Signed-off-by: Norio Nomura --- hack/convert-diffdisk-to-asif.sh | 60 -------------- pkg/driverutil/disk.go | 45 ++++++++++- pkg/imgutil/manager.go | 3 + .../nativeimgutil/asifutil/asif_darwin.go | 51 ++++++++++++ .../nativeimgutil/asifutil/asif_others.go | 25 ++++++ pkg/imgutil/nativeimgutil/fuzz_test.go | 2 +- pkg/imgutil/nativeimgutil/nativeimgutil.go | 79 +++++++++++++++++-- .../nativeimgutil/nativeimgutil_test.go | 8 +- pkg/imgutil/proxyimgutil/proxyimgutil.go | 5 ++ pkg/qemuimgutil/qemuimgutil.go | 5 ++ .../en/docs/config/environment-variables.md | 8 ++ 11 files changed, 216 insertions(+), 75 deletions(-) delete mode 100755 hack/convert-diffdisk-to-asif.sh create mode 100644 pkg/imgutil/nativeimgutil/asifutil/asif_darwin.go create mode 100644 pkg/imgutil/nativeimgutil/asifutil/asif_others.go diff --git a/hack/convert-diffdisk-to-asif.sh b/hack/convert-diffdisk-to-asif.sh deleted file mode 100755 index b5088e213fe..00000000000 --- a/hack/convert-diffdisk-to-asif.sh +++ /dev/null @@ -1,60 +0,0 @@ -#!/bin/bash - -# SPDX-FileCopyrightText: Copyright The Lima Authors -# SPDX-License-Identifier: Apache-2.0 - -# This script converts the diffdisk of a Lima instance to ASIF format. -# It requires that the instance is stopped before conversion. -# Usage: hack/convert-diffdisk-to-asif.sh [instance-name] - -set -eux -o pipefail - -instance="${1:-asif-test}" - -# Get instance dir -instance_dir=$(limactl list "${instance}" --format "{{.Dir}}") || { - echo "Failed to get instance dir for ${instance}" - exit 1 -} - -# Check diffdisk type -head4bytes="$(head -c 4 "${instance_dir}/diffdisk")" -case "${head4bytes}" in -shdw) - echo "diffdisk is already in ASIF format" - exit 1 - ;; -QFI*) - echo "diffdisk is in QCOW2 format" - exit 1 - ;; -*) ;; -esac - -# Check instance state -instance_state="$(limactl list "${instance}" --format "{{.Status}}")" || { - echo "Failed to get instance state for ${instance}" - exit 1 -} -[[ ${instance_state} == "Stopped" ]] || { - echo "Instance ${instance} must be stopped" - exit 1 -} - -# Create ASIF image -diskutil image create blank --fs none --format ASIF --size 100GiB "${instance_dir}/diffdisk.asif" - -# Attach ASIF image (`hdiutil attach` does not support attaching ASIF) -attached_device=$(diskutil image attach -n "${instance_dir}/diffdisk.asif") - -# Write `diffdisk` content to attached device using `dd` with `conv=sparse` option (`diskutil` does not support sparse) -dd if="${instance_dir}/diffdisk" of="${attached_device}" status=progress conv=sparse - -# Detach the device (`diskutil unmountDisk` does not detach the device) -hdiutil detach "${attached_device}" - -# Replace `diffdisk` with `diffdisk.asif` -mv "${instance_dir}/diffdisk" "${instance_dir}/diffdisk.raw" -mv "${instance_dir}/diffdisk.asif" "${instance_dir}/diffdisk" - -echo "Converted diffdisk to ASIF format successfully" diff --git a/pkg/driverutil/disk.go b/pkg/driverutil/disk.go index 4cf6f87b329..29e45e751fe 100644 --- a/pkg/driverutil/disk.go +++ b/pkg/driverutil/disk.go @@ -9,13 +9,17 @@ import ( "fmt" "os" "path/filepath" + "strconv" + "github.com/coreos/go-semver/semver" "github.com/docker/go-units" + "github.com/sirupsen/logrus" "github.com/lima-vm/lima/v2/pkg/imgutil/proxyimgutil" "github.com/lima-vm/lima/v2/pkg/iso9660util" "github.com/lima-vm/lima/v2/pkg/limatype" "github.com/lima-vm/lima/v2/pkg/limatype/filenames" + "github.com/lima-vm/lima/v2/pkg/osutil" ) func EnsureDiskRaw(ctx context.Context, inst *limatype.Instance) error { @@ -51,8 +55,45 @@ func EnsureDiskRaw(ctx context.Context, inst *limatype.Instance) error { } return diffDiskF.Close() } - if err = diskUtil.ConvertToRaw(ctx, baseDisk, diffDisk, &diskSize, false); err != nil { - return fmt.Errorf("failed to convert %q to a raw disk %q: %w", baseDisk, diffDisk, err) + // Check whether to use ASIF format + converter := diskUtil.ConvertToASIF + if !determineUseASIF() { + converter = diskUtil.ConvertToRaw + } + if err = converter(ctx, baseDisk, diffDisk, &diskSize, false); err != nil { + return fmt.Errorf("failed to convert %q to a disk %q: %w", baseDisk, diffDisk, err) } return err } + +func determineUseASIF() bool { + var useASIF bool + if macOSProductVersion, err := osutil.ProductVersion(); err != nil { + logrus.WithError(err).Warn("Failed to get macOS product version; using raw format instead of ASIF") + } else if macOSProductVersion.LessThan(*semver.New("26.0.0")) { + logrus.Infof("macOS version %q does not support ASIF format; using raw format instead", macOSProductVersion) + } else { + // TODO: change default to true, + // if the conversion from ASIF to raw while preserving sparsity is implemented, + // or if enough testing is done to confirm that interoperability issues won't happen with ASIF. + useASIF = false + // allow overriding via LIMA_VZ_ASIF environment variable + if envVar := os.Getenv("LIMA_VZ_ASIF"); envVar != "" { + if b, err := strconv.ParseBool(envVar); err != nil { + logrus.WithError(err).Warnf("invalid LIMA_VZ_ASIF value %q", envVar) + } else { + useASIF = b + uses := "ASIF" + if !useASIF { + uses = "raw" + } + logrus.Infof("LIMA_VZ_ASIF=%s; using %s format to diff disk", envVar, uses) + } + } else if useASIF { + logrus.Info("using ASIF format for the disk image") + } else { + logrus.Info("using raw format for the disk image") + } + } + return useASIF +} diff --git a/pkg/imgutil/manager.go b/pkg/imgutil/manager.go index ffad032d003..047b9b6751a 100644 --- a/pkg/imgutil/manager.go +++ b/pkg/imgutil/manager.go @@ -21,4 +21,7 @@ type ImageDiskManager interface { // MakeSparse makes a file sparse, starting from the specified offset. MakeSparse(ctx context.Context, f *os.File, offset int64) error + + // ConvertToASIF converts a disk image to ASIF format. + ConvertToASIF(ctx context.Context, source, dest string, size *int64, allowSourceWithBackingFile bool) error } diff --git a/pkg/imgutil/nativeimgutil/asifutil/asif_darwin.go b/pkg/imgutil/nativeimgutil/asifutil/asif_darwin.go new file mode 100644 index 00000000000..ad9ab3f7415 --- /dev/null +++ b/pkg/imgutil/nativeimgutil/asifutil/asif_darwin.go @@ -0,0 +1,51 @@ +// SPDX-FileCopyrightText: Copyright The Lima Authors +// SPDX-License-Identifier: Apache-2.0 + +package asifutil + +import ( + "context" + "fmt" + "os" + "os/exec" + "strings" +) + +// NewAttachedASIF creates a new ASIF image file at the specified path with the given size +// and attaches it, returning the attached device path and an open file handle. +// The caller is responsible for detaching the ASIF image device when done. +func NewAttachedASIF(path string, size int64) (string, *os.File, error) { + createArgs := []string{"image", "create", "blank", "--fs", "none", "--format", "ASIF", "--size", fmt.Sprintf("%d", size), path} + if err := exec.CommandContext(context.Background(), "diskutil", createArgs...).Run(); err != nil { + return "", nil, fmt.Errorf("failed to create ASIF image %q: %w", path, err) + } + attachArgs := []string{"image", "attach", "--noMount", path} + out, err := exec.CommandContext(context.Background(), "diskutil", attachArgs...).Output() + if err != nil { + return "", nil, fmt.Errorf("failed to attach ASIF image %q: %w", path, err) + } + devicePath := strings.TrimSpace(string(out)) + f, err := os.OpenFile(devicePath, os.O_RDWR, 0o644) + if err != nil { + _ = DetachASIF(devicePath) + return "", nil, fmt.Errorf("failed to open ASIF device %q: %w", devicePath, err) + } + return devicePath, f, err +} + +// DetachASIF detaches the ASIF image device at the specified path. +func DetachASIF(devicePath string) error { + if output, err := exec.CommandContext(context.Background(), "hdiutil", "detach", devicePath).CombinedOutput(); err != nil { + return fmt.Errorf("failed to detach ASIF image %q: %w: %s", devicePath, err, output) + } + return nil +} + +// ResizeASIF resizes the ASIF image at the specified path to the given size. +func ResizeASIF(path string, size int64) error { + resizeArgs := []string{"image", "resize", "--size", fmt.Sprintf("%d", size), path} + if output, err := exec.CommandContext(context.Background(), "diskutil", resizeArgs...).CombinedOutput(); err != nil { + return fmt.Errorf("failed to resize ASIF image %q: %w: %s", path, err, output) + } + return nil +} diff --git a/pkg/imgutil/nativeimgutil/asifutil/asif_others.go b/pkg/imgutil/nativeimgutil/asifutil/asif_others.go new file mode 100644 index 00000000000..7298a5e7164 --- /dev/null +++ b/pkg/imgutil/nativeimgutil/asifutil/asif_others.go @@ -0,0 +1,25 @@ +//go:build !darwin + +// SPDX-FileCopyrightText: Copyright The Lima Authors +// SPDX-License-Identifier: Apache-2.0 + +package asifutil + +import ( + "errors" + "os" +) + +var ErrASIFNotSupported = errors.New("ASIF is only supported on macOS") + +func NewAttachedASIF(_ string, _ int64) (string, *os.File, error) { + return "", nil, ErrASIFNotSupported +} + +func DetachASIF(_ string) error { + return ErrASIFNotSupported +} + +func ResizeASIF(_ string, _ int64) error { + return ErrASIFNotSupported +} diff --git a/pkg/imgutil/nativeimgutil/fuzz_test.go b/pkg/imgutil/nativeimgutil/fuzz_test.go index 204d5583fdc..421438bf91d 100644 --- a/pkg/imgutil/nativeimgutil/fuzz_test.go +++ b/pkg/imgutil/nativeimgutil/fuzz_test.go @@ -17,6 +17,6 @@ func FuzzConvertToRaw(f *testing.F) { destPath := filepath.Join(t.TempDir(), "dest.img") err := os.WriteFile(srcPath, imgData, 0o600) assert.NilError(t, err) - _ = convertToRaw(srcPath, destPath, &size, withBacking) + _ = convertTo(imageRaw, srcPath, destPath, &size, withBacking) }) } diff --git a/pkg/imgutil/nativeimgutil/nativeimgutil.go b/pkg/imgutil/nativeimgutil/nativeimgutil.go index 2eb1e2cae0b..8ab605d26d5 100644 --- a/pkg/imgutil/nativeimgutil/nativeimgutil.go +++ b/pkg/imgutil/nativeimgutil/nativeimgutil.go @@ -10,6 +10,8 @@ import ( "fmt" "io" "io/fs" + "math" + "math/rand/v2" "os" "path/filepath" @@ -17,10 +19,12 @@ import ( "github.com/docker/go-units" "github.com/lima-vm/go-qcow2reader" "github.com/lima-vm/go-qcow2reader/convert" + "github.com/lima-vm/go-qcow2reader/image/asif" "github.com/lima-vm/go-qcow2reader/image/qcow2" "github.com/lima-vm/go-qcow2reader/image/raw" "github.com/sirupsen/logrus" + "github.com/lima-vm/lima/v2/pkg/imgutil/nativeimgutil/asifutil" "github.com/lima-vm/lima/v2/pkg/progressbar" ) @@ -38,10 +42,17 @@ func roundUp(size int64) int64 { return sectors * sectorSize } -// convertToRaw converts a source disk into a raw disk. +type targetImageType string + +const ( + imageRaw targetImageType = "raw" + imageASIF targetImageType = "ASIF" +) + +// convertTo converts a source disk into a raw or ASIF disk. // source and dest may be same. -// convertToRaw is a NOP if source == dest, and no resizing is needed. -func convertToRaw(source, dest string, size *int64, allowSourceWithBackingFile bool) error { +// convertTo is a NOP if source == dest, and no resizing is needed. +func convertTo(destType targetImageType, source, dest string, size *int64, allowSourceWithBackingFile bool) error { srcF, err := os.Open(source) if err != nil { return err @@ -54,13 +65,15 @@ func convertToRaw(source, dest string, size *int64, allowSourceWithBackingFile b if size != nil && *size < srcImg.Size() { return fmt.Errorf("specified size %d is smaller than the original image size (%d) of %q", *size, srcImg.Size(), source) } - logrus.Infof("Converting %q (%s) to a raw disk %q", source, srcImg.Type(), dest) + logrus.Infof("Converting %q (%s) to a %s disk %q", source, srcImg.Type(), destType, dest) switch t := srcImg.Type(); t { case raw.Type: if err = srcF.Close(); err != nil { return err } - return convertRawToRaw(source, dest, size) + if destType == imageRaw { + return convertRawToRaw(source, dest, size) + } case qcow2.Type: if !allowSourceWithBackingFile { q, ok := srcImg.(*qcow2.Qcow2) @@ -71,6 +84,11 @@ func convertToRaw(source, dest string, size *int64, allowSourceWithBackingFile b return fmt.Errorf("qcow2 image %q has an unexpected backing file: %q", source, q.BackingFile) } } + case asif.Type: + if destType == imageASIF { + return convertASIFToASIF(source, dest, size) + } + return fmt.Errorf("conversion from ASIF to %q is not supported", destType) default: logrus.Warnf("image %q has an unexpected format: %q", source, t) } @@ -79,11 +97,26 @@ func convertToRaw(source, dest string, size *int64, allowSourceWithBackingFile b } // Create a tmp file because source and dest can be same. - destTmpF, err := os.CreateTemp(filepath.Dir(dest), filepath.Base(dest)+".lima-*.tmp") + var ( + destTmpF *os.File + destTmp string + attachedDevice string + ) + switch destType { + case imageRaw: + destTmpF, err = os.CreateTemp(filepath.Dir(dest), filepath.Base(dest)+".lima-*.tmp") + destTmp = destTmpF.Name() + case imageASIF: + // destTmp != destTmpF.Name() because destTmpF is mounted ASIF device file. + randomBase := fmt.Sprintf("%s.lima-%d.tmp.asif", filepath.Base(dest), rand.UintN(math.MaxUint)) + destTmp = filepath.Join(filepath.Dir(dest), randomBase) + attachedDevice, destTmpF, err = asifutil.NewAttachedASIF(destTmp, srcImg.Size()) + default: + return fmt.Errorf("unsupported target image type: %q", destType) + } if err != nil { return err } - destTmp := destTmpF.Name() defer os.RemoveAll(destTmp) defer destTmpF.Close() @@ -116,6 +149,13 @@ func convertToRaw(source, dest string, size *int64, allowSourceWithBackingFile b if err = destTmpF.Close(); err != nil { return err } + // Detach ASIF device + if destType == imageASIF { + err := asifutil.DetachASIF(attachedDevice) + if err != nil { + return fmt.Errorf("failed to detach ASIF image %q: %w", attachedDevice, err) + } + } // Rename destTmp into dest if err = os.RemoveAll(dest); err != nil { @@ -149,6 +189,24 @@ func convertRawToRaw(source, dest string, size *int64) error { return nil } +func convertASIFToASIF(source, dest string, size *int64) error { + if source != dest { + if err := containerdfs.CopyFile(dest, source); err != nil { + return fmt.Errorf("failed to copy %q into %q: %w", source, dest, err) + } + if err := os.Chmod(dest, 0o644); err != nil { + return fmt.Errorf("failed to set permissions on %q: %w", dest, err) + } + } + if size != nil { + logrus.Infof("Resizing to %s", units.BytesSize(float64(*size))) + if err := asifutil.ResizeASIF(dest, *size); err != nil { + return fmt.Errorf("failed to resize ASIF image %q: %w", dest, err) + } + } + return nil +} + func makeSparse(f *os.File, offset int64) error { if _, err := f.Seek(offset, io.SeekStart); err != nil { return err @@ -172,7 +230,7 @@ func (n *NativeImageUtil) CreateDisk(_ context.Context, disk string, size int64) // ConvertToRaw converts a disk image to raw format. func (n *NativeImageUtil) ConvertToRaw(_ context.Context, source, dest string, size *int64, allowSourceWithBackingFile bool) error { - return convertToRaw(source, dest, size, allowSourceWithBackingFile) + return convertTo(imageRaw, source, dest, size, allowSourceWithBackingFile) } // ResizeDisk resizes an existing disk image to the specified size. @@ -185,3 +243,8 @@ func (n *NativeImageUtil) ResizeDisk(_ context.Context, disk string, size int64) func (n *NativeImageUtil) MakeSparse(_ context.Context, f *os.File, offset int64) error { return makeSparse(f, offset) } + +// ConvertToASIF converts a disk image to ASIF format. +func (n *NativeImageUtil) ConvertToASIF(_ context.Context, source, dest string, size *int64, allowSourceWithBackingFile bool) error { + return convertTo(imageASIF, source, dest, size, allowSourceWithBackingFile) +} diff --git a/pkg/imgutil/nativeimgutil/nativeimgutil_test.go b/pkg/imgutil/nativeimgutil/nativeimgutil_test.go index 4cf9c515c68..60f54399a45 100644 --- a/pkg/imgutil/nativeimgutil/nativeimgutil_test.go +++ b/pkg/imgutil/nativeimgutil/nativeimgutil_test.go @@ -65,7 +65,7 @@ func TestConvertToRaw(t *testing.T) { t.Run("qcow without backing file", func(t *testing.T) { resultImage := filepath.Join(tmpDir, strings.ReplaceAll(strings.ReplaceAll(t.Name(), string(os.PathSeparator), "_"), "/", "_")) - err = convertToRaw(qcowImage.Name(), resultImage, nil, false) + err = convertTo(imageRaw, qcowImage.Name(), resultImage, nil, false) assert.NilError(t, err) assertFileEquals(t, rawImage.Name(), resultImage) }) @@ -73,7 +73,7 @@ func TestConvertToRaw(t *testing.T) { t.Run("qcow with backing file", func(t *testing.T) { resultImage := filepath.Join(tmpDir, strings.ReplaceAll(strings.ReplaceAll(t.Name(), string(os.PathSeparator), "_"), "/", "_")) - err = convertToRaw(qcowImage.Name(), resultImage, nil, true) + err = convertTo(imageRaw, qcowImage.Name(), resultImage, nil, true) assert.NilError(t, err) assertFileEquals(t, rawImage.Name(), resultImage) }) @@ -82,7 +82,7 @@ func TestConvertToRaw(t *testing.T) { resultImage := filepath.Join(tmpDir, strings.ReplaceAll(strings.ReplaceAll(t.Name(), string(os.PathSeparator), "_"), "/", "_")) size := int64(2_097_152) // 2mb - err = convertToRaw(qcowImage.Name(), resultImage, &size, false) + err = convertTo(imageRaw, qcowImage.Name(), resultImage, &size, false) assert.NilError(t, err) assertFileEquals(t, rawImageExtended.Name(), resultImage) }) @@ -90,7 +90,7 @@ func TestConvertToRaw(t *testing.T) { t.Run("raw", func(t *testing.T) { resultImage := filepath.Join(tmpDir, strings.ReplaceAll(strings.ReplaceAll(t.Name(), string(os.PathSeparator), "_"), "/", "_")) - err = convertToRaw(rawImage.Name(), resultImage, nil, false) + err = convertTo(imageRaw, rawImage.Name(), resultImage, nil, false) assert.NilError(t, err) assertFileEquals(t, rawImage.Name(), resultImage) }) diff --git a/pkg/imgutil/proxyimgutil/proxyimgutil.go b/pkg/imgutil/proxyimgutil/proxyimgutil.go index e08a5d2e2d1..ef8545c7e52 100644 --- a/pkg/imgutil/proxyimgutil/proxyimgutil.go +++ b/pkg/imgutil/proxyimgutil/proxyimgutil.go @@ -74,3 +74,8 @@ func (p *ImageDiskManager) MakeSparse(ctx context.Context, f *os.File, offset in } return err } + +func (p *ImageDiskManager) ConvertToASIF(ctx context.Context, source, dest string, size *int64, allowSourceWithBackingFile bool) error { + // ASIF conversion is only supported by the native image utility. + return p.native.ConvertToASIF(ctx, source, dest, size, allowSourceWithBackingFile) +} diff --git a/pkg/qemuimgutil/qemuimgutil.go b/pkg/qemuimgutil/qemuimgutil.go index 7e364cb0ac2..272f1510adc 100644 --- a/pkg/qemuimgutil/qemuimgutil.go +++ b/pkg/qemuimgutil/qemuimgutil.go @@ -269,3 +269,8 @@ func AcceptableAsBaseDisk(info *Info) error { } return nil } + +func (q *QemuImageUtil) ConvertToASIF(_ context.Context, _, _ string, _ *int64, _ bool) error { + // Should never be called because ASIF is not supported by qemu-img. + return nil +} diff --git a/website/content/en/docs/config/environment-variables.md b/website/content/en/docs/config/environment-variables.md index 7881d5eddf4..6a078056f62 100644 --- a/website/content/en/docs/config/environment-variables.md +++ b/website/content/en/docs/config/environment-variables.md @@ -139,6 +139,14 @@ This page documents the environment variables used in Lima. ```sh export LIMA_USERNET_RESOLVE_IP_ADDRESS_TIMEOUT=5 ``` +### `LIMA_VZ_ASIF` + +- **Description**: Specifies whether to use ASIF disk image format for VZ driver on macOS 26.0 or later. +- **Default**: `false` +- **Usage**: + ```sh + export LIMA_VZ_ASIF=true + ``` ### `_LIMA_QEMU_UEFI_IN_BIOS` From 2806030b513cd4ba97e410ba8b5545e9e5d6a5de Mon Sep 17 00:00:00 2001 From: Norio Nomura Date: Tue, 18 Nov 2025 18:00:34 +0900 Subject: [PATCH 3/6] Add `vmType.vz.diskImageFormat` that accepts "raw" or "asif" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add `vmType.vz.diskImageFormat` that accepts "raw" or "asif" ```yaml vmOpts: vz: diskImageFormat: null # Specify the disk image format: "raw" or "asif". # Currently only applies to the primary disk image. # "asif" requires macOS 26+, and does not support converting back to "raw". # 🟢 Builtin default: "raw" ``` - Dropped `LIMA_VZ_ASIF` environment variable Signed-off-by: Norio Nomura # Conflicts: # pkg/driver/vz/vz_driver_darwin.go # pkg/driverutil/disk.go --- .../krunkit/krunkit_driver_darwin_arm64.go | 3 +- pkg/driver/vz/vz_driver_darwin.go | 37 +++++++++++-- pkg/driverutil/disk.go | 55 ++++--------------- pkg/imgutil/nativeimgutil/fuzz_test.go | 3 +- pkg/imgutil/nativeimgutil/nativeimgutil.go | 24 +++----- .../nativeimgutil/nativeimgutil_test.go | 9 +-- pkg/limatype/lima_yaml.go | 4 +- pkg/limayaml/marshal_test.go | 3 + pkg/qemuimgutil/qemuimgutil.go | 2 +- templates/default.yaml | 5 ++ .../en/docs/config/environment-variables.md | 8 --- 11 files changed, 72 insertions(+), 81 deletions(-) diff --git a/pkg/driver/krunkit/krunkit_driver_darwin_arm64.go b/pkg/driver/krunkit/krunkit_driver_darwin_arm64.go index b0969735567..25d3ffeff10 100644 --- a/pkg/driver/krunkit/krunkit_driver_darwin_arm64.go +++ b/pkg/driver/krunkit/krunkit_driver_darwin_arm64.go @@ -17,6 +17,7 @@ import ( "time" "github.com/coreos/go-semver/semver" + "github.com/lima-vm/go-qcow2reader/image/raw" "github.com/sirupsen/logrus" "github.com/lima-vm/lima/v2/pkg/driver" @@ -60,7 +61,7 @@ func (l *LimaKrunkitDriver) Configure(inst *limatype.Instance) *driver.Configure func (l *LimaKrunkitDriver) CreateDisk(ctx context.Context) error { // Krunkit also supports qcow2 disks but raw is faster to create and use. - return driverutil.EnsureDiskRaw(ctx, l.Instance) + return driverutil.EnsureDisk(ctx, l.Instance.Dir, *l.Instance.Config.Disk, raw.Type) } func (l *LimaKrunkitDriver) Start(ctx context.Context) (chan error, error) { diff --git a/pkg/driver/vz/vz_driver_darwin.go b/pkg/driver/vz/vz_driver_darwin.go index 26291ddb7e1..4931f1b13fe 100644 --- a/pkg/driver/vz/vz_driver_darwin.go +++ b/pkg/driver/vz/vz_driver_darwin.go @@ -17,6 +17,9 @@ import ( "github.com/Code-Hex/vz/v3" "github.com/coreos/go-semver/semver" + "github.com/lima-vm/go-qcow2reader/image" + "github.com/lima-vm/go-qcow2reader/image/asif" + "github.com/lima-vm/go-qcow2reader/image/raw" "github.com/sirupsen/logrus" "github.com/lima-vm/lima/v2/pkg/driver" @@ -75,11 +78,12 @@ const Enabled = true type LimaVzDriver struct { Instance *limatype.Instance - SSHLocalPort int - vSockPort int - virtioPort string - rosettaEnabled bool - rosettaBinFmt bool + SSHLocalPort int + vSockPort int + virtioPort string + rosettaEnabled bool + rosettaBinFmt bool + diskImageFormat image.Type machine *virtualMachineWrapper waitSSHLocalPortAccessible <-chan any @@ -125,6 +129,11 @@ func (l *LimaVzDriver) Configure(inst *limatype.Instance) *driver.ConfiguredDriv if vzOpts.Rosetta.BinFmt != nil { l.rosettaBinFmt = *vzOpts.Rosetta.BinFmt } + if vzOpts.DiskImageFormat != nil { + l.diskImageFormat = *vzOpts.DiskImageFormat + } else { + l.diskImageFormat = raw.Type + } return &driver.ConfiguredDriver{ Driver: l, @@ -161,6 +170,9 @@ func (l *LimaVzDriver) FillConfig(ctx context.Context, cfg *limatype.LimaYAML, _ if vzOpts.Rosetta.BinFmt == nil { vzOpts.Rosetta.BinFmt = ptr.Of(false) } + if vzOpts.DiskImageFormat == nil { + vzOpts.DiskImageFormat = ptr.Of(raw.Type) + } var opts any if err := limayaml.Convert(vzOpts, &opts, ""); err != nil { @@ -286,6 +298,19 @@ func validateConfig(_ context.Context, cfg *limatype.LimaYAML) error { default: logrus.Warnf("field `video.display` must be \"vz\", \"default\", or \"none\" for VZ driver , got %q", videoDisplay) } + var vzOpts limatype.VZOpts + if err := limayaml.Convert(cfg.VMOpts[limatype.VZ], &vzOpts, "vmOpts.vz"); err != nil { + logrus.WithError(err).Warnf("Couldn't convert %q", cfg.VMOpts[limatype.VZ]) + } + switch *vzOpts.DiskImageFormat { + case raw.Type: + case asif.Type: + if macOSProductVersion.LessThan(*semver.New("26.0.0")) { + return fmt.Errorf("vmOpts.vz.diskImageFormat=%q requires macOS 26 or higher to run, got %q", asif.Type, macOSProductVersion) + } + default: + return fmt.Errorf("field `vmOpts.vz.diskImageFormat` must be %q or %q, got %q", raw.Type, asif.Type, *vzOpts.DiskImageFormat) + } return nil } @@ -295,7 +320,7 @@ func (l *LimaVzDriver) Create(_ context.Context) error { } func (l *LimaVzDriver) CreateDisk(ctx context.Context) error { - return driverutil.EnsureDiskRaw(ctx, l.Instance) + return driverutil.EnsureDisk(ctx, l.Instance.Dir, *l.Instance.Config.Disk, l.diskImageFormat) } func (l *LimaVzDriver) Start(ctx context.Context) (chan error, error) { diff --git a/pkg/driverutil/disk.go b/pkg/driverutil/disk.go index 29e45e751fe..99dc33d9283 100644 --- a/pkg/driverutil/disk.go +++ b/pkg/driverutil/disk.go @@ -9,21 +9,19 @@ import ( "fmt" "os" "path/filepath" - "strconv" - "github.com/coreos/go-semver/semver" "github.com/docker/go-units" - "github.com/sirupsen/logrus" + "github.com/lima-vm/go-qcow2reader/image" + "github.com/lima-vm/go-qcow2reader/image/asif" "github.com/lima-vm/lima/v2/pkg/imgutil/proxyimgutil" "github.com/lima-vm/lima/v2/pkg/iso9660util" - "github.com/lima-vm/lima/v2/pkg/limatype" "github.com/lima-vm/lima/v2/pkg/limatype/filenames" - "github.com/lima-vm/lima/v2/pkg/osutil" ) -func EnsureDiskRaw(ctx context.Context, inst *limatype.Instance) error { - diffDisk := filepath.Join(inst.Dir, filenames.DiffDisk) +// EnsureDisk ensures that the diff disk exists with the specified size and format. +func EnsureDisk(ctx context.Context, instDir, diskSize string, diskImageFormat image.Type) error { + diffDisk := filepath.Join(instDir, filenames.DiffDisk) if _, err := os.Stat(diffDisk); err == nil || !errors.Is(err, os.ErrNotExist) { // disk is already ensured return err @@ -31,10 +29,10 @@ func EnsureDiskRaw(ctx context.Context, inst *limatype.Instance) error { diskUtil := proxyimgutil.NewDiskUtil(ctx) - baseDisk := filepath.Join(inst.Dir, filenames.BaseDisk) + baseDisk := filepath.Join(instDir, filenames.BaseDisk) - diskSize, _ := units.RAMInBytes(*inst.Config.Disk) - if diskSize == 0 { + diskSizeInBytes, _ := units.RAMInBytes(diskSize) + if diskSizeInBytes == 0 { return nil } isBaseDiskISO, err := iso9660util.IsISO9660(baseDisk) @@ -56,44 +54,13 @@ func EnsureDiskRaw(ctx context.Context, inst *limatype.Instance) error { return diffDiskF.Close() } // Check whether to use ASIF format + converter := diskUtil.ConvertToASIF - if !determineUseASIF() { + if diskImageFormat != asif.Type { converter = diskUtil.ConvertToRaw } - if err = converter(ctx, baseDisk, diffDisk, &diskSize, false); err != nil { + if err = converter(ctx, baseDisk, diffDisk, &diskSizeInBytes, false); err != nil { return fmt.Errorf("failed to convert %q to a disk %q: %w", baseDisk, diffDisk, err) } return err } - -func determineUseASIF() bool { - var useASIF bool - if macOSProductVersion, err := osutil.ProductVersion(); err != nil { - logrus.WithError(err).Warn("Failed to get macOS product version; using raw format instead of ASIF") - } else if macOSProductVersion.LessThan(*semver.New("26.0.0")) { - logrus.Infof("macOS version %q does not support ASIF format; using raw format instead", macOSProductVersion) - } else { - // TODO: change default to true, - // if the conversion from ASIF to raw while preserving sparsity is implemented, - // or if enough testing is done to confirm that interoperability issues won't happen with ASIF. - useASIF = false - // allow overriding via LIMA_VZ_ASIF environment variable - if envVar := os.Getenv("LIMA_VZ_ASIF"); envVar != "" { - if b, err := strconv.ParseBool(envVar); err != nil { - logrus.WithError(err).Warnf("invalid LIMA_VZ_ASIF value %q", envVar) - } else { - useASIF = b - uses := "ASIF" - if !useASIF { - uses = "raw" - } - logrus.Infof("LIMA_VZ_ASIF=%s; using %s format to diff disk", envVar, uses) - } - } else if useASIF { - logrus.Info("using ASIF format for the disk image") - } else { - logrus.Info("using raw format for the disk image") - } - } - return useASIF -} diff --git a/pkg/imgutil/nativeimgutil/fuzz_test.go b/pkg/imgutil/nativeimgutil/fuzz_test.go index 421438bf91d..a3a6091a231 100644 --- a/pkg/imgutil/nativeimgutil/fuzz_test.go +++ b/pkg/imgutil/nativeimgutil/fuzz_test.go @@ -8,6 +8,7 @@ import ( "path/filepath" "testing" + "github.com/lima-vm/go-qcow2reader/image/raw" "gotest.tools/v3/assert" ) @@ -17,6 +18,6 @@ func FuzzConvertToRaw(f *testing.F) { destPath := filepath.Join(t.TempDir(), "dest.img") err := os.WriteFile(srcPath, imgData, 0o600) assert.NilError(t, err) - _ = convertTo(imageRaw, srcPath, destPath, &size, withBacking) + _ = convertTo(raw.Type, srcPath, destPath, &size, withBacking) }) } diff --git a/pkg/imgutil/nativeimgutil/nativeimgutil.go b/pkg/imgutil/nativeimgutil/nativeimgutil.go index 8ab605d26d5..aabcde38738 100644 --- a/pkg/imgutil/nativeimgutil/nativeimgutil.go +++ b/pkg/imgutil/nativeimgutil/nativeimgutil.go @@ -19,6 +19,7 @@ import ( "github.com/docker/go-units" "github.com/lima-vm/go-qcow2reader" "github.com/lima-vm/go-qcow2reader/convert" + "github.com/lima-vm/go-qcow2reader/image" "github.com/lima-vm/go-qcow2reader/image/asif" "github.com/lima-vm/go-qcow2reader/image/qcow2" "github.com/lima-vm/go-qcow2reader/image/raw" @@ -42,17 +43,10 @@ func roundUp(size int64) int64 { return sectors * sectorSize } -type targetImageType string - -const ( - imageRaw targetImageType = "raw" - imageASIF targetImageType = "ASIF" -) - // convertTo converts a source disk into a raw or ASIF disk. // source and dest may be same. // convertTo is a NOP if source == dest, and no resizing is needed. -func convertTo(destType targetImageType, source, dest string, size *int64, allowSourceWithBackingFile bool) error { +func convertTo(destType image.Type, source, dest string, size *int64, allowSourceWithBackingFile bool) error { srcF, err := os.Open(source) if err != nil { return err @@ -71,7 +65,7 @@ func convertTo(destType targetImageType, source, dest string, size *int64, allow if err = srcF.Close(); err != nil { return err } - if destType == imageRaw { + if destType == raw.Type { return convertRawToRaw(source, dest, size) } case qcow2.Type: @@ -85,7 +79,7 @@ func convertTo(destType targetImageType, source, dest string, size *int64, allow } } case asif.Type: - if destType == imageASIF { + if destType == asif.Type { return convertASIFToASIF(source, dest, size) } return fmt.Errorf("conversion from ASIF to %q is not supported", destType) @@ -103,10 +97,10 @@ func convertTo(destType targetImageType, source, dest string, size *int64, allow attachedDevice string ) switch destType { - case imageRaw: + case raw.Type: destTmpF, err = os.CreateTemp(filepath.Dir(dest), filepath.Base(dest)+".lima-*.tmp") destTmp = destTmpF.Name() - case imageASIF: + case asif.Type: // destTmp != destTmpF.Name() because destTmpF is mounted ASIF device file. randomBase := fmt.Sprintf("%s.lima-%d.tmp.asif", filepath.Base(dest), rand.UintN(math.MaxUint)) destTmp = filepath.Join(filepath.Dir(dest), randomBase) @@ -150,7 +144,7 @@ func convertTo(destType targetImageType, source, dest string, size *int64, allow return err } // Detach ASIF device - if destType == imageASIF { + if destType == asif.Type { err := asifutil.DetachASIF(attachedDevice) if err != nil { return fmt.Errorf("failed to detach ASIF image %q: %w", attachedDevice, err) @@ -230,7 +224,7 @@ func (n *NativeImageUtil) CreateDisk(_ context.Context, disk string, size int64) // ConvertToRaw converts a disk image to raw format. func (n *NativeImageUtil) ConvertToRaw(_ context.Context, source, dest string, size *int64, allowSourceWithBackingFile bool) error { - return convertTo(imageRaw, source, dest, size, allowSourceWithBackingFile) + return convertTo(raw.Type, source, dest, size, allowSourceWithBackingFile) } // ResizeDisk resizes an existing disk image to the specified size. @@ -246,5 +240,5 @@ func (n *NativeImageUtil) MakeSparse(_ context.Context, f *os.File, offset int64 // ConvertToASIF converts a disk image to ASIF format. func (n *NativeImageUtil) ConvertToASIF(_ context.Context, source, dest string, size *int64, allowSourceWithBackingFile bool) error { - return convertTo(imageASIF, source, dest, size, allowSourceWithBackingFile) + return convertTo(asif.Type, source, dest, size, allowSourceWithBackingFile) } diff --git a/pkg/imgutil/nativeimgutil/nativeimgutil_test.go b/pkg/imgutil/nativeimgutil/nativeimgutil_test.go index 60f54399a45..fa053bc4272 100644 --- a/pkg/imgutil/nativeimgutil/nativeimgutil_test.go +++ b/pkg/imgutil/nativeimgutil/nativeimgutil_test.go @@ -11,6 +11,7 @@ import ( "strings" "testing" + "github.com/lima-vm/go-qcow2reader/image/raw" "gotest.tools/v3/assert" ) @@ -65,7 +66,7 @@ func TestConvertToRaw(t *testing.T) { t.Run("qcow without backing file", func(t *testing.T) { resultImage := filepath.Join(tmpDir, strings.ReplaceAll(strings.ReplaceAll(t.Name(), string(os.PathSeparator), "_"), "/", "_")) - err = convertTo(imageRaw, qcowImage.Name(), resultImage, nil, false) + err = convertTo(raw.Type, qcowImage.Name(), resultImage, nil, false) assert.NilError(t, err) assertFileEquals(t, rawImage.Name(), resultImage) }) @@ -73,7 +74,7 @@ func TestConvertToRaw(t *testing.T) { t.Run("qcow with backing file", func(t *testing.T) { resultImage := filepath.Join(tmpDir, strings.ReplaceAll(strings.ReplaceAll(t.Name(), string(os.PathSeparator), "_"), "/", "_")) - err = convertTo(imageRaw, qcowImage.Name(), resultImage, nil, true) + err = convertTo(raw.Type, qcowImage.Name(), resultImage, nil, true) assert.NilError(t, err) assertFileEquals(t, rawImage.Name(), resultImage) }) @@ -82,7 +83,7 @@ func TestConvertToRaw(t *testing.T) { resultImage := filepath.Join(tmpDir, strings.ReplaceAll(strings.ReplaceAll(t.Name(), string(os.PathSeparator), "_"), "/", "_")) size := int64(2_097_152) // 2mb - err = convertTo(imageRaw, qcowImage.Name(), resultImage, &size, false) + err = convertTo(raw.Type, qcowImage.Name(), resultImage, &size, false) assert.NilError(t, err) assertFileEquals(t, rawImageExtended.Name(), resultImage) }) @@ -90,7 +91,7 @@ func TestConvertToRaw(t *testing.T) { t.Run("raw", func(t *testing.T) { resultImage := filepath.Join(tmpDir, strings.ReplaceAll(strings.ReplaceAll(t.Name(), string(os.PathSeparator), "_"), "/", "_")) - err = convertTo(imageRaw, rawImage.Name(), resultImage, nil, false) + err = convertTo(raw.Type, rawImage.Name(), resultImage, nil, false) assert.NilError(t, err) assertFileEquals(t, rawImage.Name(), resultImage) }) diff --git a/pkg/limatype/lima_yaml.go b/pkg/limatype/lima_yaml.go index 0c6e2dfa311..3e5ce38f2e3 100644 --- a/pkg/limatype/lima_yaml.go +++ b/pkg/limatype/lima_yaml.go @@ -7,6 +7,7 @@ import ( "net" "runtime" + "github.com/lima-vm/go-qcow2reader/image" "github.com/opencontainers/go-digest" "github.com/sirupsen/logrus" "golang.org/x/sys/cpu" @@ -118,7 +119,8 @@ type QEMUOpts struct { } type VZOpts struct { - Rosetta Rosetta `yaml:"rosetta,omitempty" json:"rosetta,omitempty"` + Rosetta Rosetta `yaml:"rosetta,omitempty" json:"rosetta,omitempty"` + DiskImageFormat *image.Type `yaml:"diskImageFormat,omitempty" json:"diskImageFormat,omitempty" jsonschema:"nullable"` } type Rosetta struct { diff --git a/pkg/limayaml/marshal_test.go b/pkg/limayaml/marshal_test.go index f70d6960794..014ee994980 100644 --- a/pkg/limayaml/marshal_test.go +++ b/pkg/limayaml/marshal_test.go @@ -108,6 +108,7 @@ func TestVZOpts(t *testing.T) { vmType: "vz" vmOpts: vz: + diskImageFormat: null rosetta: enabled: null binfmt: null @@ -148,6 +149,7 @@ func TestVZOptsRosettaMessage(t *testing.T) { vmType: "vz" vmOpts: vz: + diskImageFormat: "raw" rosetta: enabled: true binfmt: false @@ -160,6 +162,7 @@ message: | want := `vmType: vz vmOpts: vz: + diskImageFormat: raw rosetta: binfmt: false enabled: true diff --git a/pkg/qemuimgutil/qemuimgutil.go b/pkg/qemuimgutil/qemuimgutil.go index 272f1510adc..61d6856add6 100644 --- a/pkg/qemuimgutil/qemuimgutil.go +++ b/pkg/qemuimgutil/qemuimgutil.go @@ -272,5 +272,5 @@ func AcceptableAsBaseDisk(info *Info) error { func (q *QemuImageUtil) ConvertToASIF(_ context.Context, _, _ string, _ *int64, _ bool) error { // Should never be called because ASIF is not supported by qemu-img. - return nil + return errors.New("unimplemented") } diff --git a/templates/default.yaml b/templates/default.yaml index f3347adc7c7..23587fe8121 100644 --- a/templates/default.yaml +++ b/templates/default.yaml @@ -370,6 +370,11 @@ vmOpts: # riscv64: "max" # (or "host" when running on riscv64 host) # x86_64: "max" # (or "host" when running on x86_64 host; additional options are appended on Intel Mac) vz: + diskImageFormat: null + # Specify the disk image format: "raw" or "asif". + # Currently only applies to the primary disk image. + # "asif" requires macOS 26+, and does not support converting back to "raw". + # 🟢 Builtin default: "raw" rosetta: # Enable Rosetta inside the VM; needs `vmType: vz` # Hint: try `softwareupdate --install-rosetta` if Lima gets stuck at `Installing rosetta...` diff --git a/website/content/en/docs/config/environment-variables.md b/website/content/en/docs/config/environment-variables.md index 6a078056f62..7881d5eddf4 100644 --- a/website/content/en/docs/config/environment-variables.md +++ b/website/content/en/docs/config/environment-variables.md @@ -139,14 +139,6 @@ This page documents the environment variables used in Lima. ```sh export LIMA_USERNET_RESOLVE_IP_ADDRESS_TIMEOUT=5 ``` -### `LIMA_VZ_ASIF` - -- **Description**: Specifies whether to use ASIF disk image format for VZ driver on macOS 26.0 or later. -- **Default**: `false` -- **Usage**: - ```sh - export LIMA_VZ_ASIF=true - ``` ### `_LIMA_QEMU_UEFI_IN_BIOS` From 70ed498726dfe56aab522d0c8a67eb7345edbfca Mon Sep 17 00:00:00 2001 From: Norio Nomura Date: Wed, 19 Nov 2025 13:47:53 +0900 Subject: [PATCH 4/6] pkg/imgutil/nativeimgutil: Avoid resizing ASIF image Signed-off-by: Norio Nomura --- pkg/imgutil/nativeimgutil/nativeimgutil.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/pkg/imgutil/nativeimgutil/nativeimgutil.go b/pkg/imgutil/nativeimgutil/nativeimgutil.go index aabcde38738..e6d2671febe 100644 --- a/pkg/imgutil/nativeimgutil/nativeimgutil.go +++ b/pkg/imgutil/nativeimgutil/nativeimgutil.go @@ -104,7 +104,15 @@ func convertTo(destType image.Type, source, dest string, size *int64, allowSourc // destTmp != destTmpF.Name() because destTmpF is mounted ASIF device file. randomBase := fmt.Sprintf("%s.lima-%d.tmp.asif", filepath.Base(dest), rand.UintN(math.MaxUint)) destTmp = filepath.Join(filepath.Dir(dest), randomBase) - attachedDevice, destTmpF, err = asifutil.NewAttachedASIF(destTmp, srcImg.Size()) + // Since qcow2 image is smaller than expected size, we need to specify expected size to avoid resize later. + // Resizing ASIF image is not supported by qemu-img which recognizes ASIF format as raw. + var newSize int64 + if size != nil { + newSize = *size + } else { + newSize = srcImg.Size() + } + attachedDevice, destTmpF, err = asifutil.NewAttachedASIF(destTmp, newSize) default: return fmt.Errorf("unsupported target image type: %q", destType) } From 1d0ac3fb43e1031c6e04798ca6f276c356dd51ad Mon Sep 17 00:00:00 2001 From: Norio Nomura Date: Wed, 19 Nov 2025 14:07:19 +0900 Subject: [PATCH 5/6] pkg/imgutil: Consolidate `ConvertToRaw()` and `ConvertToASIF()` to `Convert()` Signed-off-by: Norio Nomura --- pkg/driver/krunkit/krunkit_darwin_arm64.go | 3 ++- pkg/driver/vz/vm_darwin.go | 2 +- pkg/driverutil/disk.go | 7 +---- pkg/imgutil/manager.go | 10 ++++---- pkg/imgutil/nativeimgutil/nativeimgutil.go | 12 +++------ pkg/imgutil/proxyimgutil/proxyimgutil.go | 30 ++++++++++++---------- pkg/qemuimgutil/qemuimgutil.go | 11 +++----- 7 files changed, 33 insertions(+), 42 deletions(-) diff --git a/pkg/driver/krunkit/krunkit_darwin_arm64.go b/pkg/driver/krunkit/krunkit_darwin_arm64.go index 86d3d581b3e..4f13554f1cb 100644 --- a/pkg/driver/krunkit/krunkit_darwin_arm64.go +++ b/pkg/driver/krunkit/krunkit_darwin_arm64.go @@ -14,6 +14,7 @@ import ( "strconv" "github.com/docker/go-units" + "github.com/lima-vm/go-qcow2reader/image/raw" "github.com/sirupsen/logrus" "github.com/lima-vm/lima/v2/pkg/driver/vz" @@ -65,7 +66,7 @@ func Cmdline(inst *limatype.Instance) (*exec.Cmd, error) { } extraDiskPath := filepath.Join(disk.Dir, filenames.DataDisk) logrus.Infof("Mounting disk %q on %q", disk.Name, disk.MountPoint) - if cerr := diskUtil.ConvertToRaw(ctx, extraDiskPath, extraDiskPath, nil, true); cerr != nil { + if cerr := diskUtil.Convert(ctx, raw.Type, extraDiskPath, extraDiskPath, nil, true); cerr != nil { return nil, fmt.Errorf("failed to convert extra disk %q to raw: %w", extraDiskPath, cerr) } args = append(args, "--device", fmt.Sprintf("virtio-blk,path=%s,format=raw", extraDiskPath)) diff --git a/pkg/driver/vz/vm_darwin.go b/pkg/driver/vz/vm_darwin.go index cbce17b6a5a..d0de0063467 100644 --- a/pkg/driver/vz/vm_darwin.go +++ b/pkg/driver/vz/vm_darwin.go @@ -520,7 +520,7 @@ func attachDisks(ctx context.Context, inst *limatype.Instance, vmConfig *vz.Virt // ConvertToRaw is a NOP if no conversion is needed logrus.Debugf("Converting extra disk %q to a raw disk (if it is not a raw)", extraDiskPath) - if err = diskUtil.ConvertToRaw(ctx, extraDiskPath, extraDiskPath, nil, true); err != nil { + if err = diskUtil.Convert(ctx, raw.Type, extraDiskPath, extraDiskPath, nil, true); err != nil { return fmt.Errorf("failed to convert extra disk %q to a raw disk: %w", extraDiskPath, err) } extraDiskPathAttachment, err := vz.NewDiskImageStorageDeviceAttachmentWithCacheAndSync(extraDiskPath, false, diskImageCachingMode, vz.DiskImageSynchronizationModeFsync) diff --git a/pkg/driverutil/disk.go b/pkg/driverutil/disk.go index 99dc33d9283..a9ab18ef311 100644 --- a/pkg/driverutil/disk.go +++ b/pkg/driverutil/disk.go @@ -12,7 +12,6 @@ import ( "github.com/docker/go-units" "github.com/lima-vm/go-qcow2reader/image" - "github.com/lima-vm/go-qcow2reader/image/asif" "github.com/lima-vm/lima/v2/pkg/imgutil/proxyimgutil" "github.com/lima-vm/lima/v2/pkg/iso9660util" @@ -55,11 +54,7 @@ func EnsureDisk(ctx context.Context, instDir, diskSize string, diskImageFormat i } // Check whether to use ASIF format - converter := diskUtil.ConvertToASIF - if diskImageFormat != asif.Type { - converter = diskUtil.ConvertToRaw - } - if err = converter(ctx, baseDisk, diffDisk, &diskSizeInBytes, false); err != nil { + if err = diskUtil.Convert(ctx, diskImageFormat, baseDisk, diffDisk, &diskSizeInBytes, false); err != nil { return fmt.Errorf("failed to convert %q to a disk %q: %w", baseDisk, diffDisk, err) } return err diff --git a/pkg/imgutil/manager.go b/pkg/imgutil/manager.go index 047b9b6751a..1c94484b9aa 100644 --- a/pkg/imgutil/manager.go +++ b/pkg/imgutil/manager.go @@ -6,6 +6,8 @@ package imgutil import ( "context" "os" + + "github.com/lima-vm/go-qcow2reader/image" ) // ImageDiskManager defines the common operations for disk image utilities. @@ -16,12 +18,10 @@ type ImageDiskManager interface { // ResizeDisk resizes an existing disk image to the specified size. ResizeDisk(ctx context.Context, disk string, size int64) error - // ConvertToRaw converts a disk image to raw format. - ConvertToRaw(ctx context.Context, source, dest string, size *int64, allowSourceWithBackingFile bool) error + // Convert converts a disk image to the specified format. + // Currently supported formats are raw.Type and asif.Type. + Convert(ctx context.Context, imageType image.Type, source, dest string, size *int64, allowSourceWithBackingFile bool) error // MakeSparse makes a file sparse, starting from the specified offset. MakeSparse(ctx context.Context, f *os.File, offset int64) error - - // ConvertToASIF converts a disk image to ASIF format. - ConvertToASIF(ctx context.Context, source, dest string, size *int64, allowSourceWithBackingFile bool) error } diff --git a/pkg/imgutil/nativeimgutil/nativeimgutil.go b/pkg/imgutil/nativeimgutil/nativeimgutil.go index e6d2671febe..59e45c15267 100644 --- a/pkg/imgutil/nativeimgutil/nativeimgutil.go +++ b/pkg/imgutil/nativeimgutil/nativeimgutil.go @@ -230,9 +230,10 @@ func (n *NativeImageUtil) CreateDisk(_ context.Context, disk string, size int64) return f.Truncate(roundedSize) } -// ConvertToRaw converts a disk image to raw format. -func (n *NativeImageUtil) ConvertToRaw(_ context.Context, source, dest string, size *int64, allowSourceWithBackingFile bool) error { - return convertTo(raw.Type, source, dest, size, allowSourceWithBackingFile) +// Convert converts a disk image to the specified format. +// Currently supported formats are raw.Type and asif.Type. +func (n *NativeImageUtil) Convert(_ context.Context, imageType image.Type, source, dest string, size *int64, allowSourceWithBackingFile bool) error { + return convertTo(imageType, source, dest, size, allowSourceWithBackingFile) } // ResizeDisk resizes an existing disk image to the specified size. @@ -245,8 +246,3 @@ func (n *NativeImageUtil) ResizeDisk(_ context.Context, disk string, size int64) func (n *NativeImageUtil) MakeSparse(_ context.Context, f *os.File, offset int64) error { return makeSparse(f, offset) } - -// ConvertToASIF converts a disk image to ASIF format. -func (n *NativeImageUtil) ConvertToASIF(_ context.Context, source, dest string, size *int64, allowSourceWithBackingFile bool) error { - return convertTo(asif.Type, source, dest, size, allowSourceWithBackingFile) -} diff --git a/pkg/imgutil/proxyimgutil/proxyimgutil.go b/pkg/imgutil/proxyimgutil/proxyimgutil.go index ef8545c7e52..09ad8150afb 100644 --- a/pkg/imgutil/proxyimgutil/proxyimgutil.go +++ b/pkg/imgutil/proxyimgutil/proxyimgutil.go @@ -9,6 +9,9 @@ import ( "os" "os/exec" + "github.com/lima-vm/go-qcow2reader/image" + "github.com/lima-vm/go-qcow2reader/image/raw" + "github.com/lima-vm/lima/v2/pkg/imgutil" "github.com/lima-vm/lima/v2/pkg/imgutil/nativeimgutil" "github.com/lima-vm/lima/v2/pkg/qemuimgutil" @@ -52,16 +55,20 @@ func (p *ImageDiskManager) ResizeDisk(ctx context.Context, disk string, size int return err } -// ConvertToRaw converts a disk image to raw format. -func (p *ImageDiskManager) ConvertToRaw(ctx context.Context, source, dest string, size *int64, allowSourceWithBackingFile bool) error { - err := p.qemu.ConvertToRaw(ctx, source, dest, size, allowSourceWithBackingFile) - if err == nil { - return nil - } - if errors.Is(err, exec.ErrNotFound) { - return p.native.ConvertToRaw(ctx, source, dest, size, allowSourceWithBackingFile) +// Convert converts a disk image to the specified format. +// Currently supported formats are raw.Type and asif.Type. +func (p *ImageDiskManager) Convert(ctx context.Context, imageType image.Type, source, dest string, size *int64, allowSourceWithBackingFile bool) error { + if imageType == raw.Type { + err := p.qemu.Convert(ctx, imageType, source, dest, size, allowSourceWithBackingFile) + if err == nil { + return nil + } + if errors.Is(err, exec.ErrNotFound) { + return p.native.Convert(ctx, imageType, source, dest, size, allowSourceWithBackingFile) + } + return err } - return err + return p.native.Convert(ctx, imageType, source, dest, size, allowSourceWithBackingFile) } func (p *ImageDiskManager) MakeSparse(ctx context.Context, f *os.File, offset int64) error { @@ -74,8 +81,3 @@ func (p *ImageDiskManager) MakeSparse(ctx context.Context, f *os.File, offset in } return err } - -func (p *ImageDiskManager) ConvertToASIF(ctx context.Context, source, dest string, size *int64, allowSourceWithBackingFile bool) error { - // ASIF conversion is only supported by the native image utility. - return p.native.ConvertToASIF(ctx, source, dest, size, allowSourceWithBackingFile) -} diff --git a/pkg/qemuimgutil/qemuimgutil.go b/pkg/qemuimgutil/qemuimgutil.go index 61d6856add6..dc8900e1c1b 100644 --- a/pkg/qemuimgutil/qemuimgutil.go +++ b/pkg/qemuimgutil/qemuimgutil.go @@ -14,6 +14,7 @@ import ( "os/exec" "strconv" + "github.com/lima-vm/go-qcow2reader/image" "github.com/sirupsen/logrus" ) @@ -199,8 +200,9 @@ func GetInfo(ctx context.Context, path string) (*Info, error) { return qemuInfo, nil } -// ConvertToRaw converts a disk image to raw format. -func (q *QemuImageUtil) ConvertToRaw(ctx context.Context, source, dest string, size *int64, allowSourceWithBackingFile bool) error { +// Convert converts a disk image to raw format. +// Specified imageType is ignored. +func (q *QemuImageUtil) Convert(ctx context.Context, _ image.Type, source, dest string, size *int64, allowSourceWithBackingFile bool) error { if !allowSourceWithBackingFile { info, err := getInfo(ctx, source) if err != nil { @@ -269,8 +271,3 @@ func AcceptableAsBaseDisk(info *Info) error { } return nil } - -func (q *QemuImageUtil) ConvertToASIF(_ context.Context, _, _ string, _ *int64, _ bool) error { - // Should never be called because ASIF is not supported by qemu-img. - return errors.New("unimplemented") -} From cbe724175131421dd9efc530710ca6ae28eb2d88 Mon Sep 17 00:00:00 2001 From: Norio Nomura Date: Wed, 19 Nov 2025 14:36:14 +0900 Subject: [PATCH 6/6] Apply reviews - default.yaml: Fix comment placing - pkg/qemuimgutil: Change `QemuImageUtil.Convert` to returns error on specifying other than `raw.Type` - pkg/imgutil: Drop supported image format information from doc comment of `ImageDiskManager.Convert` Signed-off-by: Norio Nomura --- pkg/imgutil/manager.go | 1 - pkg/qemuimgutil/qemuimgutil.go | 8 ++++++-- templates/default.yaml | 2 +- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/pkg/imgutil/manager.go b/pkg/imgutil/manager.go index 1c94484b9aa..ba48b9e3d6b 100644 --- a/pkg/imgutil/manager.go +++ b/pkg/imgutil/manager.go @@ -19,7 +19,6 @@ type ImageDiskManager interface { ResizeDisk(ctx context.Context, disk string, size int64) error // Convert converts a disk image to the specified format. - // Currently supported formats are raw.Type and asif.Type. Convert(ctx context.Context, imageType image.Type, source, dest string, size *int64, allowSourceWithBackingFile bool) error // MakeSparse makes a file sparse, starting from the specified offset. diff --git a/pkg/qemuimgutil/qemuimgutil.go b/pkg/qemuimgutil/qemuimgutil.go index dc8900e1c1b..14ad4d47293 100644 --- a/pkg/qemuimgutil/qemuimgutil.go +++ b/pkg/qemuimgutil/qemuimgutil.go @@ -15,6 +15,7 @@ import ( "strconv" "github.com/lima-vm/go-qcow2reader/image" + "github.com/lima-vm/go-qcow2reader/image/raw" "github.com/sirupsen/logrus" ) @@ -201,8 +202,11 @@ func GetInfo(ctx context.Context, path string) (*Info, error) { } // Convert converts a disk image to raw format. -// Specified imageType is ignored. -func (q *QemuImageUtil) Convert(ctx context.Context, _ image.Type, source, dest string, size *int64, allowSourceWithBackingFile bool) error { +// Currently only raw.Type is supported. +func (q *QemuImageUtil) Convert(ctx context.Context, imageType image.Type, source, dest string, size *int64, allowSourceWithBackingFile bool) error { + if imageType != raw.Type { + return fmt.Errorf("QemuImageUtil.Convert only supports raw.Type, got %q", imageType) + } if !allowSourceWithBackingFile { info, err := getInfo(ctx, source) if err != nil { diff --git a/templates/default.yaml b/templates/default.yaml index 23587fe8121..fde32308a0e 100644 --- a/templates/default.yaml +++ b/templates/default.yaml @@ -370,11 +370,11 @@ vmOpts: # riscv64: "max" # (or "host" when running on riscv64 host) # x86_64: "max" # (or "host" when running on x86_64 host; additional options are appended on Intel Mac) vz: - diskImageFormat: null # Specify the disk image format: "raw" or "asif". # Currently only applies to the primary disk image. # "asif" requires macOS 26+, and does not support converting back to "raw". # 🟢 Builtin default: "raw" + diskImageFormat: null rosetta: # Enable Rosetta inside the VM; needs `vmType: vz` # Hint: try `softwareupdate --install-rosetta` if Lima gets stuck at `Installing rosetta...`