From a86f7f8c748c4ea582224678bf63b4df64ff8c52 Mon Sep 17 00:00:00 2001 From: Norio Nomura Date: Mon, 10 Nov 2025 19:15:05 +0900 Subject: [PATCH 1/4] 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 --- go.mod | 2 ++ go.sum | 4 +-- hack/convert-diffdisk-to-asif.sh | 56 ++++++++++++++++++++++++++++++++ pkg/driver/vz/vm_darwin.go | 3 +- 4 files changed, 62 insertions(+), 3 deletions(-) create mode 100755 hack/convert-diffdisk-to-asif.sh diff --git a/go.mod b/go.mod index 7158ea1dfd0..c21b119c1a9 100644 --- a/go.mod +++ b/go.mod @@ -146,3 +146,5 @@ require ( sigs.k8s.io/randfill v1.0.0 // indirect sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect ) + +replace github.com/lima-vm/go-qcow2reader => github.com/norio-nomura/go-qcow2reader v0.6.1-0.20251110085027-73026bc16b7b diff --git a/go.sum b/go.sum index e50e5a6415e..30bf0ec929b 100644 --- a/go.sum +++ b/go.sum @@ -162,8 +162,6 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/lima-vm/go-qcow2reader v0.6.0 h1:dNstUGQxEUPbmiiVnu/cek2x7scrHe2VJy5JseLLflo= -github.com/lima-vm/go-qcow2reader v0.6.0/go.mod h1:ay45SlGOzU+2Vc21g5/lmQgPn7Hmf0JpPhm8cuOK1FI= github.com/lima-vm/sshocker v0.3.8 h1:nnIaqi1G1hvWihm++53YBXeI8/x7CQNzhEswPC0M2E0= github.com/lima-vm/sshocker v0.3.8/go.mod h1:sO9dTE0i+I0BHHVpyoKZWkPFtHtTkrqcfCkQguRMsx8= github.com/linuxkit/virtsock v0.0.0-20220523201153-1a23e78aa7a2 h1:DZMFueDbfz6PNc1GwDRA8+6lBx1TB9UnxDQliCqR73Y= @@ -209,6 +207,8 @@ github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFd github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/norio-nomura/go-qcow2reader v0.6.1-0.20251110085027-73026bc16b7b h1:oP4ZY9Geumc+PmknrDpdXJc3V4XFVLHVuv3/sUhIlB4= +github.com/norio-nomura/go-qcow2reader v0.6.1-0.20251110085027-73026bc16b7b/go.mod h1:ay45SlGOzU+2Vc21g5/lmQgPn7Hmf0JpPhm8cuOK1FI= github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY= github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= diff --git a/hack/convert-diffdisk-to-asif.sh b/hack/convert-diffdisk-to-asif.sh new file mode 100755 index 00000000000..915f0a5b312 --- /dev/null +++ b/hack/convert-diffdisk-to-asif.sh @@ -0,0 +1,56 @@ +#!/bin/bash + +# SPDX-FileCopyrightText: Copyright The Lima Authors +# SPDX-License-Identifier: Apache-2.0 + +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..1874d517dd9 100644 --- a/pkg/driver/vz/vm_darwin.go +++ b/pkg/driver/vz/vm_darwin.go @@ -22,6 +22,7 @@ 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/asif" "github.com/lima-vm/go-qcow2reader/image/raw" "github.com/sirupsen/logrus" @@ -451,7 +452,7 @@ 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 { + if t := img.Type(); t != raw.Type && t != asif.Type { return fmt.Errorf("expected the format of %q to be %q, got %q", diskPath, raw.Type, t) } // TODO: ensure that the disk is formatted with GPT or ISO9660 From ea726cb9adb816a387dad333b87c211df3343675 Mon Sep 17 00:00:00 2001 From: Norio Nomura Date: Tue, 11 Nov 2025 00:34:42 +0900 Subject: [PATCH 2/4] 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 | 4 ++++ pkg/driver/vz/vm_darwin.go | 7 +++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/hack/convert-diffdisk-to-asif.sh b/hack/convert-diffdisk-to-asif.sh index 915f0a5b312..b5088e213fe 100755 --- a/hack/convert-diffdisk-to-asif.sh +++ b/hack/convert-diffdisk-to-asif.sh @@ -3,6 +3,10 @@ # 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}" diff --git a/pkg/driver/vz/vm_darwin.go b/pkg/driver/vz/vm_darwin.go index 1874d517dd9..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,7 @@ 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" @@ -452,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 && t != asif.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 a92ccafff9f34183be9488811eb0dbac0505e4a0 Mon Sep 17 00:00:00 2001 From: Norio Nomura Date: Tue, 11 Nov 2025 08:53:51 +0900 Subject: [PATCH 3/4] Bump github.com/lima-vm/go-qcow2reader to v0.7.0 Signed-off-by: Norio Nomura --- go.mod | 4 +--- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/go.mod b/go.mod index c21b119c1a9..10059ee4b31 100644 --- a/go.mod +++ b/go.mod @@ -25,7 +25,7 @@ require ( github.com/google/go-cmp v0.7.0 github.com/google/yamlfmt v0.20.0 github.com/invopop/jsonschema v0.13.0 - github.com/lima-vm/go-qcow2reader v0.6.0 + github.com/lima-vm/go-qcow2reader v0.7.0 github.com/lima-vm/sshocker v0.3.8 // gomodjail:unconfined github.com/mattn/go-isatty v0.0.20 github.com/mattn/go-shellwords v1.0.12 @@ -146,5 +146,3 @@ require ( sigs.k8s.io/randfill v1.0.0 // indirect sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect ) - -replace github.com/lima-vm/go-qcow2reader => github.com/norio-nomura/go-qcow2reader v0.6.1-0.20251110085027-73026bc16b7b diff --git a/go.sum b/go.sum index 30bf0ec929b..f6250e8701a 100644 --- a/go.sum +++ b/go.sum @@ -162,6 +162,8 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lima-vm/go-qcow2reader v0.7.0 h1:p+t0U7aRyEl5cc7t5Z54XzbaYeLTy4s3bn+Wl6+j4iA= +github.com/lima-vm/go-qcow2reader v0.7.0/go.mod h1:ay45SlGOzU+2Vc21g5/lmQgPn7Hmf0JpPhm8cuOK1FI= github.com/lima-vm/sshocker v0.3.8 h1:nnIaqi1G1hvWihm++53YBXeI8/x7CQNzhEswPC0M2E0= github.com/lima-vm/sshocker v0.3.8/go.mod h1:sO9dTE0i+I0BHHVpyoKZWkPFtHtTkrqcfCkQguRMsx8= github.com/linuxkit/virtsock v0.0.0-20220523201153-1a23e78aa7a2 h1:DZMFueDbfz6PNc1GwDRA8+6lBx1TB9UnxDQliCqR73Y= @@ -207,8 +209,6 @@ github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFd github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/norio-nomura/go-qcow2reader v0.6.1-0.20251110085027-73026bc16b7b h1:oP4ZY9Geumc+PmknrDpdXJc3V4XFVLHVuv3/sUhIlB4= -github.com/norio-nomura/go-qcow2reader v0.6.1-0.20251110085027-73026bc16b7b/go.mod h1:ay45SlGOzU+2Vc21g5/lmQgPn7Hmf0JpPhm8cuOK1FI= github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY= github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= From a88474b193e2dbb4d7751a646b7d41c9a1c424ec Mon Sep 17 00:00:00 2001 From: Norio Nomura Date: Tue, 11 Nov 2025 19:24:52 +0900 Subject: [PATCH 4/4] 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/driver/vz/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/driver/vz/disk.go b/pkg/driver/vz/disk.go index 9cb81e06c5c..af729a6d1eb 100644 --- a/pkg/driver/vz/disk.go +++ b/pkg/driver/vz/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 EnsureDisk(ctx context.Context, inst *limatype.Instance) error { @@ -51,8 +55,45 @@ func EnsureDisk(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`