Created: June 5, 2023

Last modified: September 16, 2024

guix system on the macos virtualization framework

Resources

filesystem corruption bug

Sway software rendering

VM’s assigned IP

Mac icons

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 

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"))
                        )))
                    )))