Useful obscure projects for MacOS virtualization:
- UTM (https://mac.getutm.app/)
- QuickEmu (https://github.com/quickemu-project)
- Tart (https://tart.run/)
Nothing works perfectly, except (I've heard) Parallels, which I haven't yet tried.
VMWare fusion sucks. Virtualbox sucks more.
UTM is quite nice. QuickEmu and Tart lack a GUI.
Tart supplies images with xcode preinstalled, so you won't need to use the app store.
Tips from QuickEmu (haven't tried them in others):
sudo trimforce enable
Enables SSD TRIM commands on the guest, helping qemu to shrink the QCOW image. Comes complete with a scary screenful of warnings that can be safely ignored, we're in a VM dude. Might not always work, google it.
MacOS VMs will usually be blocked from logging in with Apple IDs by default, due to the Serial, MAC Address, and other information being shared between multiple VMs.
To fix this, you can use this tool to replace or randomize these values in the bootloader.
https://github.com/quickemu-project/qe_mac_apid
en0
. Use ifconfig
in a terminal to verify this.If the wired ethernet device is not en0
, then then go to System Preferences -> Network, delete all the network devices and apply the
changes. Next, open a terminal and run the following:
sudo rm /Library/Preferences/SystemConfiguration/NetworkInterfaces.plist
Now reboot, and the App Store should work.
Tips from UTM:
How to resize MacOS VM image (from https://github.com/utmapp/UTM/issues/4186#issuecomment-2163220345):
If you want to be able to increase the size of your disk image while retaining the ability to upgrade your OS, the Apple_APFS_Recovery partition must be relocated to the end of the disk before expanding the main Apple_APFS container.
So far the only ready-made solution I have found is the
Tart packer plugin from the Tart project by cirruslabs which allows for
automated VM creation and has the option to automatically resize disk
images (see the recovery_partition = "relocate"
option).
More information on the projects can be found below:
https://github.com/cirruslabs/tart
https://github.com/cirruslabs/packer-plugin-tart/tree/main
Since I wanted to continue using UTM and simply needed a
standalone tool to relocate the partition, I created a small tool based
on their code. Understand that it come with no guarantee, but the code
is available here:
https://gist.github.com/cdavidc/3509ceefba518f20d1c123b6435407d6
(fully copied below because these things tend to get lost)
// Code taken from https://github.com/cirruslabs/packer-plugin-tart/blob/main/builder/tart/recoverypartition/relocate.go // to create a standalone tool to relocate Apple_APFS_Recovery partitions to the end of a macOS disk image // Build with: // go mod init RelocatePartition.go // go mod tidy // go build RelocatePartition.go // Run with: // ./RelocatePartition path_to_disk_image package main import ( "fmt" "github.com/diskfs/go-diskfs" "github.com/diskfs/go-diskfs/partition/gpt" "github.com/samber/lo" "io" "os" ) const Name = "RecoveryOSContainer" func Relocate(diskImagePath string) error { // Open the disk image and read its partition table disk, err := diskfs.Open(diskImagePath) if err != nil { return fmt.Errorf("failed to open the disk image: %w", err) } partitionTable, err := disk.GetPartitionTable() if err != nil { return fmt.Errorf("failed to get the partition table: %w", err) } // We only support relocating a recovery partition on a GPT table gptTable, ok := partitionTable.(*gpt.Table) if !ok { return fmt.Errorf("expected a \"gpt\" partition table, got %q", partitionTable.Type()) } // Find the recovery partition recoveryPartition, recoveryPartitionIndex, ok := lo.FindIndexOf(gptTable.Partitions, func(partition *gpt.Partition) bool { return partition.Name == Name }) if !ok { fmt.Println("Nothing to relocate: no recovery partition found.") return nil } // We only support relocating the recovery partition if it's the last partition on disk if (recoveryPartitionIndex + 1) != len(gptTable.Partitions) { return fmt.Errorf("cannot relocate the recovery partition since it's not the last partition " + "on disk") } // Determine the last sector available for partitions, which is normally the (total LBA - 34) sector[1] // // [1]: https://commons.wikimedia.org/wiki/File:GUID_Partition_Table_Scheme.svg lastSectorAvailableForPartitions := uint64((disk.Size / disk.LogicalBlocksize) - 34) // Perhaps the recovery partition already resides at the last sector available for partitions? if recoveryPartition.End >= lastSectorAvailableForPartitions { fmt.Println("Nothing to relocate: recovery partition already ends at the last sector available to partitions.") return nil } fmt.Println("Dumping recovery partition contents...") tmpFile, err := os.CreateTemp("", "") if err != nil { return fmt.Errorf("failed to create a temporary file for storing "+ "the recovery partition contents: %w", err) } defer os.Remove(tmpFile.Name()) if err := dumpPartition(diskImagePath, tmpFile.Name(), recoveryPartition.GetStart(), recoveryPartition.GetSize()); err != nil { return fmt.Errorf("failed to dump the recovery partition contents: %w", err) } fmt.Println("Re-partitioning the disk to adjust the recovery partition bounds...") recoveryPartitionSizeInSectors := recoveryPartition.End - recoveryPartition.Start recoveryPartition.Start = lastSectorAvailableForPartitions - recoveryPartitionSizeInSectors recoveryPartition.End = lastSectorAvailableForPartitions // Re-partition the disk with the new recovery partition bounds if err := disk.Partition(gptTable); err != nil { return fmt.Errorf("failed to write the new partition table: %w", err) } fmt.Println("Restoring recovery partition contents...") if err := restorePartition(diskImagePath, tmpFile.Name(), recoveryPartition.GetStart(), recoveryPartition.GetSize()); err != nil { return fmt.Errorf("failed to restore the recovery partition contents: %w", err) } return nil } func dumpPartition(diskFilePath string, partitionFilePath string, off int64, n int64) error { diskFile, err := os.Open(diskFilePath) if err != nil { return err } defer diskFile.Close() partitionFile, err := os.Create(partitionFilePath) if err != nil { return err } partitionContentsReader := io.NewSectionReader(diskFile, off, n) if _, err := io.Copy(partitionFile, partitionContentsReader); err != nil { return err } return nil } func restorePartition(diskFilePath string, partitionFilePath string, off int64, n int64) error { diskFile, err := os.OpenFile(diskFilePath, os.O_RDWR, 0600) if err != nil { return err } contentsFile, err := os.Open(partitionFilePath) if err != nil { return err } defer contentsFile.Close() partitionContentsWriter := io.NewOffsetWriter(diskFile, off) if _, err := io.CopyN(partitionContentsWriter, contentsFile, n); err != nil { return err } return diskFile.Close() } func main() { if len(os.Args) < 2 { fmt.Println("Usage:", os.Args[0], "path_to_disk_image") os.Exit(1) } diskImageFilePath := os.Args[1] Relocate(diskImageFilePath) }
To build it, install go (with brew for instance) and execute the few commands listed in the comments at the beginning of the file.
Considering that, the full instructions are:
- Locate the VM image in UTM by right-clicking on the VM and select "Show in Finder"
- Resize the image using
hdiutil resize -size <new_size_in_GB>g -imageonly -verbose <full_path_and_name_of_the_image_file>
- cd to where you built the RelocatePartition tool and run
./RelocatePartition <full_path_and_name_of_the_image_file>
- Close and re-open UTM, so it re-reads the VM config, then start the VM in the recovery mode
- Use
diskutil list
to get the list of the containers (the first block in the output) and located theApple_APFS
container - Resize the APFS container with
diskutil apfs resizeContainer <id_of_the_Apple_APFS_container> 0
- Restart the VM
I also disable SIP (boot into recovery, there's an option in UTM to do that by right-clicking on the VM, open a terminal, csrutil disable) then remove all apple bundled bullshit (books, tv, etc), then re-enable SIP.
Conversion between raw image and qcow2: https://github.com/utmapp/UTM/discussions/3784