The stack that enables virtualization
hardware
Apple silicon has special hardware that enables the virtualization of CPUs and memory
can run multiple OSes on top of a single SoC
macOS kernel
software that takes advantage of the hardware
no need to write kernel extensions (or KEXTs)
low-level API that lets you virtualize CPUs and memory
used to access the macOS kernel virtualization capabilities from your application
high-level API that lets you virtualize machines
enables the creation of virtual machines running macOS on Apple silicon, or Linux on both Apple silicon and Intel
Virtualization Framework
When using Virtualization framework, we’ll deal with two kinds of objects:
Configuration objects - which define all the properties of our virtual machines
Virtual machine objects - abstract virtual machines and how to interact with them
1. Configuration objects
define the hardware
creating a configuration is like configuring a Mac on the Apple Store (how many CPUs, how much memory, what kind of devices)
we can add a display, and we get to see the content
we can add a keyboard, and we can type
we can add a trackpad, and we can interact with the UI
done in code:
var configuration = VZVirtualMachineConfiguration() // 👈🏻 root object of all configurations
configuration.cpuCount = 4 // 👈🏻 4 CPUs
configuration.memorySize = (4 * 1024 * 1024 * 1024) as UInt64 // 👈🏻 4 GB of memory
configuration.storageDevices = [newBlockDevice()] // 👈🏻 one storage device
configuration.pointingDevices = [newPointingDevice()] // 👈🏻 4 GB of memory2. Virtual machine objects
running a virtual machine:
let virtualMachine = VZVirtualMachine(configuration: configuration) // 👈🏻 the configuration from before
try await virtualMachine.start() // 👈🏻 boot it upTo interact with the virtual machines, we need to use other objects from the Virtualization framework
to show a virtual display:
let virtualMachineView = VZVirtualMachineView() // 👈🏻 just a normal NSView
virtualMachineView.virtualMachine = virtualMachine
... // integrate virtualMachineView in your app to see the content of the virtual machineRun full OSes in virtual machines
macOS
Configuration
We need to set two special properties in our configuration to make a Mac virtual machine:
Platform a. hardware model - specifies which version of the virtual Mac we want. b. auxiliary storage - form of non-volatile memory used by the system c. machine identifier - unique number representing the machine
boot loader - must be macOS boot loader
// 👇🏻 same as before
var configuration = VZVirtualMachineConfiguration()
configuration.cpuCount = 4
configuration.memorySize = (4 * 1024 * 1024 * 1024) as UInt64
configuration.storageDevices = [newBlockDevice()]
configuration.pointingDevices = [newPointingDevice()]
// 👇🏻 Platform properties
let platform = VZMacPlatformConfiguration() // 👈🏻 platform object
let hardwareModel = VZMacHardwareModel(dataRepresentation: savedHardwareModel)
platform.hardwareModel = hardwareModel! // 👈🏻 hardware model
let auxiliaryStorage = VZMacAuxiliaryStorage(contentsOf: auxiliaryStorageURL)
platform.auxiliaryStorage = auxiliaryStorage // 👈🏻 auxiliary storage
let machineIdentifier = VZMacMachineIdentifier(dataRepresentation: savedIdentifier)
platform.machineIdentifier = machineIdentifier! // 👈🏻 machine identifier
configuration.platform = platform // 👈🏻 we set special properties in our configuration object
// 👇🏻 Mac Boot loader
configuration.bootLoader = VZMacOSBootLoader()How to install macOS into a vm
Three steps:
download and restore image (we can choose which maoOS version we want to install)
create configuration (compatible with image above)
run installer
// 1. get and restore image
let restoreImage = try await VZMacOSRestoreImage.latestSupported // 👈🏻 gets a restores image object for the latest stable version of macOS
// alternatively we could download from the developer portal
try await download(restoreImage.url)
// 2. create configuration
let requirements = restoreImage.mostFeaturefulSupportedConfiguration
// 👆🏻 this lists us the requirements to run on the current system
guard let requirements = requirements else {
// No compatible configuration.
return
}
platform.hardwareModel = requirements.hardwareModel
configuration.cpuCount = requirements.minimumSupportedCPUCount
configuration.memorySize = requirements.minimumSupportedMemorySize
// 3. install macOS
let virtualMachine = VZVirtualMachine(configuration: configuration)
let installer = VZMacOSInstaller(virtualMachine: virtualMachine, restoringFromImageAt: imageURL)
try await installer.install()Using your Mac
GPU acceleration (
VZMacGraphicsDisplayConfiguration)built-in graphic device that exposes the GPU capabilities to the virtual Mac
you can run Metal in the virtual machine, and get great graphics performance in macOS
let graphicsConfiguration = VZMacGraphicsDeviceConfiguration()
graphicsConfiguration.displays = [
VZMacGraphicsDisplayConfiguration(widthInPixels: 1920, heightInPixels: 1200, pixelsPerInch: 80)
// 👆🏻 screen size and pixel density
]
configuration.graphicsDevices = [graphicsConfiguration]virtual trackpad (
VZMacTrackpadConfiguration)requires macOS 13 on both host OS and virtual machine
let trackpad = VZMacTrackpadConfiguration()
configuration.pointingDevices = [trackpad]Sharing files between host OS and virtual mac
new in macOS 13 via Virtio file-system
you can choose which folders that you want to share with the virtual machine
any change you make from the host system is instantly reflected within the virtual machine and vice versa
// 👇🏻 path to the directory we want to share
let sharedDirectory = VZSharedDirectory(url: directoryURL, readOnly: false)
let share = VZSingleDirectoryShare(directory: sharedDirectory) // can use VZMultipleDirectoryShare for more directories
let tag = VZVirtioFileSystemDeviceConfiguration.macOSGuestAutomountTag // File system devices are identified by a tag
let sharingDevice = VZVirtioFileSystemDeviceConfiguration(tag: tag)
sharingDevice.share = share
configuration.directorySharingDevices = [sharingDevice]Linux
How to install linux into a vm
Three steps, like IRL:
download image
put into a usb drive (as a device in the vm)
run installer from usb drive
done via EFI boot loader
takes advantage of EFI boot discovery mechanism, which looks at each drive for one that can be booted
once detected, EFI will start the installer from the virtual usb drive
// 1. download image
let diskImageURL = URL(fileURLWithPath: "linux.iso")
// 2. put into a usb drive
// 👇🏻 A disk image attachment represents a piece of storage that we can attach to a device
let attachment = try! VZDiskImageStorageDeviceAttachment(url: diskImageURL, readOnly: true)
let usbDeviceConfiguration = VZUSBMassStorageDeviceConfiguration(attachment: attachment) // 👈🏻 our usb drive
configuration.storageDevices = [usbDeviceConfiguration, createBlockDevice()]
// 3. run installer from usb drive
let efi = VZEFIBootLoader()
// 👇🏻 EFI requires this non-volatile memory to store information between boots
efi.variableStore = VZEFIVariableStore(creatingVariableStoreAt: storeURL, options: [])
configuration.bootLoader = efi // set EFI boot loaderRunning your vm
graphics (Virtio GPU 2D)
let virtioGPU = VZVirtioGraphicsDeviceConfiguration()
virtioGPU.scanouts = [
VZVirtioGraphicsScanoutConfiguration(widthInPixels: 1280, heightInPixels: 720) // 👈🏻 virtual display
]
configuration.graphicsDevices = [virtioGPU]Rosetta 2 (macOS Ventura)
Linux binaries support
translates the Linux x86-64 binaries inside your virtual machine.
useful when developing for x86-64 servers
vm setup:
// setting up Rosetta
let rosettaDirectoryShare = try! VZLinuxRosettaDirectoryShare() // 👈🏻 special object
let directorySharingDevice = VZVirtioFileSystemDeviceConfiguration(tag: "RosettaShare")
directorySharingDevice.share = rosettaDirectoryShare
configuration.directorySharingDevices = [directorySharingDevice]in the vm, run:
mount -t virtiofs RosettaShare /mnt/Rosetta
sudo /usr/sbin/update-binfmts --install rosetta /mnt/Rosetta/rosetta \
--magic "\x7fELF\x02\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x3e\x00" \
--mask "\xff\xff\xff\xff\xff\xfe\xfe\x00\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff\xff" \
--credentials yes --preserve no --fix-binary yesupdate-binfmtstells the system to use Rosetta to handle any x86-64 binaryafter running the commands above, every x86-64 binary launched will be translated by Rosetta.
