guix system on the macos virtualization framework
Resources
Started with swift code examples from:
- https://developer.apple.com/documentation/virtualization/running_gui_linux_in_a_virtual_machine_on_a_mac
- https://github.com/traderepublic/Cilicon
- https://github.com/cirruslabs/tart/blob/546238d9df688442d1f74185aff3152408d5a358/Sources/tart/Commands/Run.swift#L483
- https://gist.github.com/karwa/5207e232ac9ec53f0276252ab5e3ee07
filesystem corruption bug
Running into kernel panic caused by file system corruption
Sway software rendering
- https://gitlab.freedesktop.org/wlroots/wlroots/-/blob/master/docs/env_vars.md
- Encountered visual glitches with llvmpipe
- pixman renderer worked better, but scaling was bad
VM’s assigned IP
- https://github.com/utmapp/UTM/issues/3294
- https://discussions.apple.com/thread/7993351
- https://github.com/lima-vm/lima/pull/1207
- Indicates multiple vm won’t be able to talk to eachother, probably not a huge issue and can always ssh bridge
Info.plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleExecutable</key>
<string>guix</string>
<key>CFBundleIconFile</key>
<string>guix.icns</string>
<key>CFBundleGetInfoString</key>
<string>guix alpha</string>
<key>CFBundleIdentifier</key>
<string>com.akirakyle.guix</string>
<key>CFBundleName</key>
<string>Guix</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
</dict>
</plist>
entitlements.plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.virtualization</key>
<true/>
</dict>
</plist>
doit.sh
swiftc vm-appkit.swift -o ../Guix.app/Contents/MacOS/guix
codesign --entitlements entitlements.plist --verify -s - ../Guix.app
ls -R Guix.app
Contents
Guix.app/Contents: Info.plist MacOS Resources _CodeSignature
Guix.app/Contents/MacOS: guix
Guix.app/Contents/Resources: guix.icns
Guix.app/Contents/CodeSignature: CodeResources
vm-appkit.swift
import AppKit
import Virtualization
let basePath = NSHomeDirectory() + "/virt/"
func vmConfig() throws -> VZVirtualMachineConfiguration {
let virtualMachineConfiguration = VZVirtualMachineConfiguration()
virtualMachineConfiguration.cpuCount = 4
virtualMachineConfiguration.memorySize = (4 * 1024 * 1024 * 1024) as UInt64 // 4 GiB
let platform = VZGenericPlatformConfiguration()
let machineIdentifierData = try Data(contentsOf: URL(fileURLWithPath: basePath + "MachineIdentifier"))
platform.machineIdentifier = VZGenericMachineIdentifier(dataRepresentation: machineIdentifierData)!
virtualMachineConfiguration.platform = platform
let bootloader = VZEFIBootLoader()
bootloader.variableStore = VZEFIVariableStore(url: URL(fileURLWithPath: basePath + "NVRAM"))
virtualMachineConfiguration.bootLoader = bootloader
let guixDiskAttachment = try VZDiskImageStorageDeviceAttachment(
url: URL(fileURLWithPath: basePath + "guix.img"), readOnly: false,
cachingMode: .cached, synchronizationMode: .full)
//let homeDiskAttachment = try VZDiskImageStorageDeviceAttachment(
// url: URL(fileURLWithPath: basePath + "Disk.img"), readOnly: false,
// cachingMode: .cached, synchronizationMode: .full)
virtualMachineConfiguration.storageDevices = [
VZVirtioBlockDeviceConfiguration(attachment: guixDiskAttachment)
//VZVirtioBlockDeviceConfiguration(attachment: homeDiskAttachment)
]
let shareURL = URL(fileURLWithPath: NSHomeDirectory() + "/vm-share")
let sharedDirectory = VZSharedDirectory(url: shareURL, readOnly: false)
let singleDirectoryShare = VZSingleDirectoryShare(directory: sharedDirectory)
let sharingConfiguration = VZVirtioFileSystemDeviceConfiguration(tag: "share")
sharingConfiguration.share = singleDirectoryShare
virtualMachineConfiguration.directorySharingDevices = [ sharingConfiguration ]
let networkDevice = VZVirtioNetworkDeviceConfiguration()
networkDevice.attachment = VZNATNetworkDeviceAttachment()
virtualMachineConfiguration.networkDevices = [networkDevice]
virtualMachineConfiguration.keyboards = [VZUSBKeyboardConfiguration()]
virtualMachineConfiguration.pointingDevices = [VZUSBScreenCoordinatePointingDeviceConfiguration()]
let graphicsDevice = VZVirtioGraphicsDeviceConfiguration()
graphicsDevice.scanouts = [
//VZVirtioGraphicsScanoutConfiguration(widthInPixels: 2880, heightInPixels: 1800)
VZVirtioGraphicsScanoutConfiguration(widthInPixels: 800, heightInPixels: 600)
]
virtualMachineConfiguration.graphicsDevices = [graphicsDevice]
try virtualMachineConfiguration.validate()
return virtualMachineConfiguration
}
class AppDelegate: NSObject, NSApplicationDelegate, VZVirtualMachineDelegate {
var vm = VZVirtualMachine(configuration: try! vmConfig())
let vmView = VZVirtualMachineView()
let window = NSWindow()
func applicationDidFinishLaunching(_ aNotification: Notification) {
Task { try await self.vm.start() }
vm.delegate = self
vmView.virtualMachine = vm
//vmView.layerContentsPlacement = NSView.LayerContentsPlacement.center
//vmView.wantsLayer = false
//self.vmView.capturesSystemKeys = true
//self.vmView.automaticallyReconfiguresDisplay = true
//print("vmView.frame.size is \(vmView.frame)")
//vmView.frame.size = NSMakeSize(1440, 900)
self.window.setContentSize(NSSize(width:800, height:600))
//self.window.setContentSize(vmView.frame.size)
self.window.styleMask = [.titled, .closable, .resizable]
//self.window.styleMask = [.titled, .fullSizeContentView]
self.window.title = "VM"
window.contentView = vmView //window.contentView?.addSubview(vmView)
window.makeKeyAndOrderFront(nil)
NSApp.activate(ignoringOtherApps: true)
print("started")
//print("vmView is \(vmView)")
//print("vmView.superview is \(vmView.superview!)")
//print("vmView.subviews is \(vmView.subviews)")
//print("vmView.frame is \(vmView.frame)")
//print("vmView.bounds is \(vmView.bounds)")
//print("centerScanRect is \(vmView.centerScanRect(vmView.frame))")
//print("layerContentsPlacement is \(vmView.layerContentsPlacement)")
//print("wantsLayer is \(vmView.wantsLayer)")
//print("autoresizingMask is \(vmView.autoresizingMask)")
//print("autoresizesSubviews is \(vmView.autoresizesSubviews)")
//vmView.layerContentsPlacement = NSView.LayerContentsPlacement.center
//print("layerContentsPlacement is \(vmView.layerContentsPlacement)")
//let fbView = vmView.subviews[0]
//print("fbView.frame is \(fbView.frame)")
//print("fbView.bounds is \(fbView.bounds)")
//print("centerScanRect is \(fbView.centerScanRect(fbView.frame))")
//print("layerContentsPlacement is \(fbView.layerContentsPlacement)")
//print("wantsLayer is \(fbView.wantsLayer)")
//print("autoresizingMask is \(fbView.autoresizingMask)")
//print("autoresizesSubviews is \(fbView.autoresizesSubviews)")
//fbView.layerContentsPlacement = NSView.LayerContentsPlacement.center
//print("layerContentsPlacement is \(fbView.layerContentsPlacement)")
}
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
return true
}
func virtualMachine(_ virtualMachine: VZVirtualMachine, networkDevice: VZNetworkDevice, attachmentWasDisconnectedWithError error: Error) {
print("network attachment disconnected with error: \(error.localizedDescription)")
}
func virtualMachine(_ virtualMachine: VZVirtualMachine, didStopWithError error: Error) {
print("vm stopped with error: \(error.localizedDescription)")
exit(-1)
}
func guestDidStop(_ virtualMachine: VZVirtualMachine) {
print("vm stopped by guest")
exit(0)
}
}
let app = NSApplication.shared
let del = AppDelegate()
app.delegate = del
app.run()
mk-vm-files.swift
import Virtualization
let basePath = NSHomeDirectory() + "/virt/"
let machineIdentifierPath = basePath + "MachineIdentifier"
let efiVariableStorePath = basePath + "NVRAM"
let mainDiskImagePath = basePath + "Disk.img"
let mainDiskSize = 32 * 1024 * 1024 * 1024 as UInt64 // 32 GiB
let machineIdentifier = VZGenericMachineIdentifier()
try machineIdentifier.dataRepresentation.write(to: URL(fileURLWithPath: machineIdentifierPath))
let efiVariableStore = try VZEFIVariableStore(creatingVariableStoreAt: URL(fileURLWithPath: efiVariableStorePath))
let diskCreated = FileManager.default.createFile(atPath: mainDiskImagePath, contents: nil, attributes: nil)
let mainDiskFileHandle = try FileHandle(forWritingTo: URL(fileURLWithPath: mainDiskImagePath))
try mainDiskFileHandle.truncate(atOffset: mainDiskSize)
Bootstrap guix
https://www.debian.org/CD/http-ftp/ used netinstaller to install debian on Disk.img
python3 -m http.server to transfer guix-sys-config
apt install parted
apt install dosfstools
apt install guix
guix pull
parted /dev/vdb
mklabel gpt
mkpart primary ext4 512MiB 100%
mkpart primary fat32 0% 512MiB
set 2 esp on
mkfs.fat -n boot -F32 /dev/vdb2
mkfs.ext4 -L guixsd /dev/vdb1
mount LABEL=guixsd /mnt
mkdir -p /mnt/boot
mount LABEL=boot /mnt/boot
wget 192.168.65.1:8000/guix-sys-config.scm
Potentially next time easier to just use my server to create the system image directly via https://guix.gnu.org/manual/devel/en/guix.html#System-Images
guix-sys-config.scm
(use-modules (gnu)
(guix packages)
(guix utils)
(gnu bootloader))
(use-service-modules linux networking ssh desktop dbus nfs)
(use-package-modules disk certs wget version-control rsync compression ssh
admin vim tmux virtualization bootloaders)
(operating-system
(host-name "data")
(timezone "America/Denver")
(locale "en_US.utf8")
(keyboard-layout (keyboard-layout "us" "dvorak"))
(bootloader (bootloader-configuration
(bootloader grub-efi-bootloader)
(targets '("/boot"))
(keyboard-layout keyboard-layout)))
(file-systems (append
(list (file-system
(device (file-system-label "boot"))
(mount-point "/boot")
(type "vfat"))
(file-system
(device (file-system-label "guixsd"))
(mount-point "/")
(type "ext4")))
%base-file-systems))
(users (cons (user-account
(name "akyle")
(group "users")
(supplementary-groups '("wheel" "audio" "video")))
%base-user-accounts))
;; Globally-installed packages.
(packages (append (list parted wget git rsync unzip openssh-sans-x
vim htop tmux)
%base-packages))
(services (append (list (service ntp-service-type)
(service dhcp-client-service-type)
(service openssh-service-type
(openssh-configuration
(openssh openssh-sans-x)
(port-number 15213)
(password-authentication? #f)
(use-pam? #f)))
)
(modify-services %base-services
(guix-service-type
config =>
(guix-configuration
(inherit config)
(extra-options '("--gc-keep-derivations=yes"
"--gc-keep-outputs=yes"))
)))
)))