Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions disk.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
package vz

import (
"context"
"fmt"
"os"
"os/exec"
"strconv"
)

// CreateDiskImage is creating disk image with specified filename and filesize.
Expand All @@ -21,3 +25,37 @@ func CreateDiskImage(pathname string, size int64) error {
}
return nil
}

// CreateSparseDiskImage is creating an "Apple Sparse Image Format" disk image
// with specified filename and filesize. The function "shells out" to diskutil, as currently
// this is the only known way of creating ASIF images.
// For example, if you want to create disk with 64GiB, you can set "64 * 1024 * 1024 * 1024" to size.
//
// Note that ASIF is only available from macOS Tahoe, so the function will return error
// on earlier versions.
func CreateSparseDiskImage(ctx context.Context, pathname string, size int64) error {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nagypeterjob Could you add test for this function?

A simple test to verify these points is sufficient:

  • Whether a disk image file is created
  • Whether the format is ASIF
  • Whether it is created at the specified size

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, I will do it in a day or two 🙇🏻

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There you go @Code-Hex, let me know if you need any changes 🙇🏻

if err := macOSAvailable(26); err != nil {
return err
}
diskutil, err := exec.LookPath("diskutil")
if err != nil {
return fmt.Errorf("failed to find disktuil: %w", err)
}

sizeStr := strconv.FormatInt(size, 10)
cmd := exec.CommandContext(ctx,
diskutil,
"image",
"create",
"blank",
"--fs", "none",
"--format", "ASIF",
"--size", sizeStr,
pathname)

if err := cmd.Run(); err != nil {
return fmt.Errorf("failed to create ASIF disk image: %w", err)
}

return nil
}
113 changes: 113 additions & 0 deletions disk_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package vz_test

import (
"context"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"testing"

"github.com/Code-Hex/vz/v3"
)

func TestCreateSparseDiskImage_FileCreated(t *testing.T) {
if vz.Available(26) {
t.Skip("CreateSparseDiskImage is supported from macOS 26")
}

dir := t.TempDir()
path := filepath.Join(dir, "sparse_disk.img")

ctx := context.Background()
size := int64(1024 * 1024 * 1024) // 1 GiB

err := vz.CreateSparseDiskImage(ctx, path, size)
if err != nil {
t.Fatalf("failed to create sparse disk image: %v", err)
}

if _, err := os.Stat(path); os.IsNotExist(err) {
t.Fatal("disk image file was not created")
}
}

func TestCreateSparseDiskImage_ASIFFormat(t *testing.T) {
if vz.Available(26) {
t.Skip("CreateSparseDiskImage is supported from macOS 26")
}

dir := t.TempDir()
path := filepath.Join(dir, "sparse_disk.img")

ctx := context.Background()
size := int64(1024 * 1024 * 1024) // 1 GiB
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Occupying 1 GB for testing is not good - can't you do something like 1 KB, 1 MB?

Copy link
Contributor

@cfergeau cfergeau Sep 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The image is supposed to be sparse, so I’d expect it will use a few MBs on disk at most.

Copy link
Contributor Author

@nagypeterjob nagypeterjob Sep 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cfergeau thats right, de "real" disk size the image claims is negligible, given its empty

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nagypeterjob I see Thanks!


err := vz.CreateSparseDiskImage(ctx, path, size)
if err != nil {
t.Fatalf("failed to create sparse disk image: %v", err)
}

// Check if the format is ASIF using diskutil
cmd := exec.Command("diskutil", "image", "info", path)
output, err := cmd.Output()
if err != nil {
t.Fatalf("failed to get disk image info: %v", err)
}

outputStr := string(output)
lines := strings.Split(outputStr, "\n")

foundASIF := false
// Check if ASIF is mentioned in the first line
if len(lines) != 0 && strings.Contains(lines[0], "ASIF") {
foundASIF = true
}

if !foundASIF {
t.Errorf("disk image is not in ASIF format. Output: %v", lines[:1])
}
}

func TestCreateSparseDiskImage_CorrectSize(t *testing.T) {
if vz.Available(26) {
t.Skip("CreateSparseDiskImage is supported from macOS 26")
}

dir := t.TempDir()
path := filepath.Join(dir, "sparse_disk.img")

ctx := context.Background()
desiredSize := int64(2 * 1024 * 1024 * 1024) // 2 GiB

err := vz.CreateSparseDiskImage(ctx, path, desiredSize)
if err != nil {
t.Fatalf("failed to create sparse disk image: %v", err)
}

cmd := exec.Command("diskutil", "image", "info", path)
output, err := cmd.Output()
if err != nil {
t.Fatalf("failed to get disk image info: %v", err)
}

var sizeStr string
for _, line := range strings.Split(string(output), "\n") {
if strings.Contains(line, "Total Bytes") {
Copy link
Contributor Author

@nagypeterjob nagypeterjob Sep 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to get the total "logical" size from the diskutil image info <image_path> output, otherwise stat.Size and even syscall.Stat_t returns the "real" size of the disk, which is close to 0 given currently empty

components := strings.Split(strings.TrimSpace(line), ":")
if len(components) > 1 {
sizeStr = strings.TrimSpace(components[1])
break
}
}
}
actualSize, err := strconv.ParseInt(sizeStr, 10, 64)
if err != nil {
t.Fatalf("failed to parse string to int: %v", err)
}

if desiredSize != actualSize {
t.Fatalf("actual disk size (%d) doesn't equal to desired size (%d)", actualSize, desiredSize)
}
}
5 changes: 3 additions & 2 deletions example/macOS/installer.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ func installMacOS(ctx context.Context) error {
}
configurationRequirements := restoreImage.MostFeaturefulSupportedConfiguration()
config, err := setupVirtualMachineWithMacOSConfigurationRequirements(
ctx,
configurationRequirements,
)
if err != nil {
Expand Down Expand Up @@ -67,12 +68,12 @@ func installMacOS(ctx context.Context) error {
return installer.Install(ctx)
}

func setupVirtualMachineWithMacOSConfigurationRequirements(macOSConfiguration *vz.MacOSConfigurationRequirements) (*vz.VirtualMachineConfiguration, error) {
func setupVirtualMachineWithMacOSConfigurationRequirements(ctx context.Context, macOSConfiguration *vz.MacOSConfigurationRequirements) (*vz.VirtualMachineConfiguration, error) {
platformConfig, err := createMacInstallerPlatformConfiguration(macOSConfiguration)
if err != nil {
return nil, fmt.Errorf("failed to create mac platform config: %w", err)
}
return setupVMConfiguration(platformConfig)
return setupVMConfiguration(ctx, platformConfig)
}

func createMacInstallerPlatformConfiguration(macOSConfiguration *vz.MacOSConfigurationRequirements) (*vz.MacPlatformConfiguration, error) {
Expand Down
26 changes: 21 additions & 5 deletions example/macOS/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,12 @@ import (

var install bool
var nbdURL string
var asifDiskImage bool

func init() {
flag.BoolVar(&install, "install", false, "run command as install mode")
flag.StringVar(&nbdURL, "nbd-url", "", "nbd url (e.g. nbd+unix:///export?socket=nbd.sock)")
flag.BoolVar(&asifDiskImage, "asif", false, "use ASIF disk image instead of raw")
}

func main() {
Expand All @@ -44,7 +46,7 @@ func runVM(ctx context.Context) error {
if err != nil {
return err
}
config, err := setupVMConfiguration(platformConfig)
config, err := setupVMConfiguration(ctx, platformConfig)
if err != nil {
return err
}
Expand Down Expand Up @@ -156,9 +158,9 @@ func computeMemorySize() uint64 {
return memorySize
}

func createBlockDeviceConfiguration(diskPath string) (*vz.VirtioBlockDeviceConfiguration, error) {
func createBlockDeviceConfiguration(ctx context.Context, diskPath string) (*vz.VirtioBlockDeviceConfiguration, error) {
// create disk image with 64 GiB
if err := vz.CreateDiskImage(diskPath, 64*1024*1024*1024); err != nil {
if err := createDiskImage(ctx, diskPath, 64*1024*1024*1024); err != nil {
if !os.IsExist(err) {
return nil, fmt.Errorf("failed to create disk image: %w", err)
}
Expand Down Expand Up @@ -265,7 +267,7 @@ func createMacPlatformConfiguration() (*vz.MacPlatformConfiguration, error) {
)
}

func setupVMConfiguration(platformConfig vz.PlatformConfiguration) (*vz.VirtualMachineConfiguration, error) {
func setupVMConfiguration(ctx context.Context, platformConfig vz.PlatformConfiguration) (*vz.VirtualMachineConfiguration, error) {
bootloader, err := vz.NewMacOSBootLoader()
if err != nil {
return nil, err
Expand All @@ -287,7 +289,7 @@ func setupVMConfiguration(platformConfig vz.PlatformConfiguration) (*vz.VirtualM
config.SetGraphicsDevicesVirtualMachineConfiguration([]vz.GraphicsDeviceConfiguration{
graphicsDeviceConfig,
})
blockDeviceConfig, err := createBlockDeviceConfiguration(GetDiskImagePath())
blockDeviceConfig, err := createBlockDeviceConfiguration(ctx, GetDiskImagePath())
if err != nil {
return nil, fmt.Errorf("failed to create block device configuration: %w", err)
}
Expand Down Expand Up @@ -363,3 +365,17 @@ func retrieveNetworkBlockDeviceStorageDeviceAttachment(storages []vz.StorageDevi
}
return nil
}

func createDiskImage(ctx context.Context, diskpath string, size int64) error {
if asifDiskImage {
if err := vz.CreateSparseDiskImage(ctx, diskpath, size); err != nil {
return err
}
return nil
}

if err := vz.CreateDiskImage(diskpath, size); err != nil {
return err
}
return nil
}