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/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/vm_darwin.go b/pkg/driver/vz/vm_darwin.go index 15cff0444e3..d0de0063467 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 @@ -516,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/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 4cf6f87b329..a9ab18ef311 100644 --- a/pkg/driverutil/disk.go +++ b/pkg/driverutil/disk.go @@ -11,15 +11,16 @@ import ( "path/filepath" "github.com/docker/go-units" + "github.com/lima-vm/go-qcow2reader/image" "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" ) -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 @@ -27,10 +28,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) @@ -51,8 +52,10 @@ 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 + + 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 ffad032d003..ba48b9e3d6b 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,8 +18,8 @@ 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. + 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 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..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) - _ = convertToRaw(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 2eb1e2cae0b..59e45c15267 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,13 @@ 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" "github.com/sirupsen/logrus" + "github.com/lima-vm/lima/v2/pkg/imgutil/nativeimgutil/asifutil" "github.com/lima-vm/lima/v2/pkg/progressbar" ) @@ -38,10 +43,10 @@ func roundUp(size int64) int64 { return sectors * sectorSize } -// convertToRaw converts a source disk into a raw disk. +// 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 image.Type, source, dest string, size *int64, allowSourceWithBackingFile bool) error { srcF, err := os.Open(source) if err != nil { return err @@ -54,13 +59,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 == raw.Type { + return convertRawToRaw(source, dest, size) + } case qcow2.Type: if !allowSourceWithBackingFile { q, ok := srcImg.(*qcow2.Qcow2) @@ -71,6 +78,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 == asif.Type { + 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 +91,34 @@ 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 raw.Type: + destTmpF, err = os.CreateTemp(filepath.Dir(dest), filepath.Base(dest)+".lima-*.tmp") + destTmp = destTmpF.Name() + 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) + // 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) + } if err != nil { return err } - destTmp := destTmpF.Name() defer os.RemoveAll(destTmp) defer destTmpF.Close() @@ -116,6 +151,13 @@ func convertToRaw(source, dest string, size *int64, allowSourceWithBackingFile b if err = destTmpF.Close(); err != nil { return err } + // Detach ASIF device + if destType == asif.Type { + 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 +191,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 @@ -170,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 convertToRaw(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. diff --git a/pkg/imgutil/nativeimgutil/nativeimgutil_test.go b/pkg/imgutil/nativeimgutil/nativeimgutil_test.go index 4cf9c515c68..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 = convertToRaw(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 = convertToRaw(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 = convertToRaw(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 = convertToRaw(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/imgutil/proxyimgutil/proxyimgutil.go b/pkg/imgutil/proxyimgutil/proxyimgutil.go index e08a5d2e2d1..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 +// 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 } - if errors.Is(err, exec.ErrNotFound) { - return p.native.ConvertToRaw(ctx, source, dest, size, allowSourceWithBackingFile) - } - 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 { 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 7e364cb0ac2..14ad4d47293 100644 --- a/pkg/qemuimgutil/qemuimgutil.go +++ b/pkg/qemuimgutil/qemuimgutil.go @@ -14,6 +14,8 @@ import ( "os/exec" "strconv" + "github.com/lima-vm/go-qcow2reader/image" + "github.com/lima-vm/go-qcow2reader/image/raw" "github.com/sirupsen/logrus" ) @@ -199,8 +201,12 @@ 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. +// 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 f3347adc7c7..fde32308a0e 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: + # 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...`