/* See LICENSE folder for this sample’s licensing information. Abstract: A command-line utility that runs Linux in a virtual machine. */ import Foundation import Virtualization class Delegate: NSObject { } class MySocketListenerDelegate: NSObject, VZVirtioSocketListenerDelegate { func listener(_ listener: VZVirtioSocketListener, shouldAcceptNewConnection connection: VZVirtioSocketConnection, from device: VZVirtioSocketDevice) -> Bool { // Implement your logic to decide whether to accept the new connection // For example, you may always accept: return true // Or, you might want to check some properties of the connection or device and decide based on that } } extension Delegate: VZVirtualMachineDelegate { func guestDidStop(_ virtualMachine: VZVirtualMachine) { print("The guest shut down. Exiting.") exit(EXIT_SUCCESS) } } if #available(macOS 13.0, *) { let vmBundlePath = NSHomeDirectory() + "/.guix/guix.bundle/" let mainDiskImagePath = vmBundlePath + "guix-raw.img" let efiVariableStorePath = vmBundlePath + "NVRAM" let machineIdentifierPath = vmBundlePath + "MachineIdentifier" var needsInstall = true // MARK: Parse the Command Line // MARK: Create the Virtual Machine Configuration print(shell("/bin/zsh ~/.guix/startup-script.sh")) if !FileManager.default.fileExists(atPath: vmBundlePath) { needsInstall = true // createVMBundle() print(shell("mkdir ~/.guix && mkdir ~/.guix/home")) print(shell("cp -r /Applications/Guix.app/Contents/Resources/home/ssh-cert ~/.guix/")) print(shell("cp /Applications/Guix.app/Contents/Resources/home/*.sh ~/.guix/")) do { try FileManager.default.createDirectory(atPath: vmBundlePath, withIntermediateDirectories: false) } catch { fatalError("Failed to create “GUI Linux VM.bundle.”") } print("The main system image is now downloading. This may take a few minutes...") // createMainDiskImage() let url = URL(string: "https://objectstorage.us-phoenix-1.oraclecloud.com/n/axfgkze2xif1/b/guix-system/o/msg-system-aarch64guix-raw.img.tar.gz") FileDownloader.loadFileSync(url: url!) { (path, error) in print("File downloaded to : \(path!)") } print(shell("tar -xvzf ~/.guix/guix.bundle/guix.tar.gz -C ~/.guix/guix.bundle/")) //print(shell("/opt/homebrew/bin/qemu-img convert ~/.guix/guix.bundle/guix-raw.qcow2 ~/.guix/guix.bundle/guix-raw.img")) // configureAndStartVirtualMachine() } else { needsInstall = false // configureAndStartVirtualMachine() } let configuration = VZVirtualMachineConfiguration() let platform = VZGenericPlatformConfiguration() let bootloader = VZEFIBootLoader() let disksArray = NSMutableArray() //set cpu count let totalAvailableCPUs = ProcessInfo.processInfo.processorCount var virtualCPUCount = totalAvailableCPUs <= 1 ? 1 : totalAvailableCPUs - 1 virtualCPUCount = max(virtualCPUCount, VZVirtualMachineConfiguration.minimumAllowedCPUCount) virtualCPUCount = min(virtualCPUCount, VZVirtualMachineConfiguration.maximumAllowedCPUCount) configuration.cpuCount = virtualCPUCount //set memory size var memorySize = (4 * 1024 * 1024 * 1024) as UInt64 // 4 GiB memorySize = max(memorySize, VZVirtualMachineConfiguration.minimumAllowedMemorySize) memorySize = min(memorySize, VZVirtualMachineConfiguration.maximumAllowedMemorySize) configuration.memorySize = memorySize // 2 GiB if needsInstall { // This is a fresh install: Create a new machine identifier and EFI variable store, // and configure a USB mass storage device to boot the ISO image. let machineIdentifier = VZGenericMachineIdentifier() // Store the machine identifier to disk so you can retrieve it for subsequent boots. try! machineIdentifier.dataRepresentation.write(to: URL(fileURLWithPath: machineIdentifierPath)) platform.machineIdentifier = machineIdentifier guard let efiVariableStore = try? VZEFIVariableStore(creatingVariableStoreAt: URL(fileURLWithPath: efiVariableStorePath)) else { fatalError("Failed to create the EFI variable store.") } bootloader.variableStore = efiVariableStore // disksArray.add(createUSBMassStorageDeviceConfiguration()) } else { // The VM is booting from a disk image that already has the OS installed. // Retrieve the machine identifier and EFI variable store that were saved to // disk during installation. // Retrieve the machine identifier. guard let machineIdentifierData = try? Data(contentsOf: URL(fileURLWithPath: machineIdentifierPath)) else { fatalError("Failed to retrieve the machine identifier data.") } guard let machineIdentifier = VZGenericMachineIdentifier(dataRepresentation: machineIdentifierData) else { fatalError("Failed to create the machine identifier.") } platform.machineIdentifier = machineIdentifier if !FileManager.default.fileExists(atPath: efiVariableStorePath) { fatalError("EFI variable store does not exist.") } bootloader.variableStore = VZEFIVariableStore(url: URL(fileURLWithPath: efiVariableStorePath)) } configuration.platform = platform configuration.bootLoader = bootloader // Disk management guard let mainDiskAttachment = try? VZDiskImageStorageDeviceAttachment(url: URL(fileURLWithPath: mainDiskImagePath), readOnly: false) else { fatalError("Failed to create main disk attachment.") } let mainDisk = VZVirtioBlockDeviceConfiguration(attachment: mainDiskAttachment) disksArray.add(mainDisk) guard let disks = disksArray as? [VZStorageDeviceConfiguration] else { fatalError("Invalid disksArray.") } configuration.storageDevices = disks // Setup network let networkDevice = VZVirtioNetworkDeviceConfiguration() networkDevice.attachment = VZNATNetworkDeviceAttachment() configuration.networkDevices = [networkDevice] // TODO: setup audioDevices // Shared dirs let projectsURL = URL(fileURLWithPath: "/Users") let sharedDirectory = VZSharedDirectory(url: projectsURL, readOnly: false) let singleDirectoryShare = VZSingleDirectoryShare(directory: sharedDirectory) // Create the VZVirtioFileSystemDeviceConfiguration and assign it a unique tag. let sharingConfiguration = VZVirtioFileSystemDeviceConfiguration(tag: "Users") sharingConfiguration.share = singleDirectoryShare configuration.directorySharingDevices = [sharingConfiguration] //configuration.serialPorts = [ createConsoleConfiguration() ] //configuration.bootLoader = createBootLoader(kernelURL: kernelURL, initialRamdiskURL: initialRamdiskURL) let inputAudioDevice = VZVirtioSoundDeviceConfiguration() let inputStream = VZVirtioSoundDeviceInputStreamConfiguration() inputStream.source = VZHostAudioInputStreamSource() inputAudioDevice.streams = [inputStream] let outputAudioDevice = VZVirtioSoundDeviceConfiguration() let outputStream = VZVirtioSoundDeviceOutputStreamConfiguration() outputStream.sink = VZHostAudioOutputStreamSink() outputAudioDevice.streams = [outputStream] configuration.audioDevices = [inputAudioDevice, outputAudioDevice] // configuration.socketDevices = [VZVirtioSocketDeviceConfiguration()] // // do { try configuration.validate() } catch { print("Failed to validate the virtual machine configuration. \(error)") exit(EXIT_FAILURE) } // MARK: Instantiate and Start the Virtual Machine let virtualMachine = VZVirtualMachine(configuration: configuration) // let socketDevice = virtualMachine.socketDevices[0] as! VZVirtioSocketDevice let delegate = Delegate() // let socketListener = VZVirtioSocketListener() // let socketDelegate = MySocketListenerDelegate() // socketListener.delegate = socketDelegate background(delay: 3.0, background: { print(shell("/bin/zsh ~/.guix/running-script.sh")) }, completion: { print("Guix is now running and Graphical apps can be forwarded (if xquartz was installed)") // when background job finishes, wait 3 seconds and do something in main thread }) DispatchQueue.main.async { virtualMachine.delegate = delegate // socketDevice.setSocketListener(socketListener, forPort: 80) virtualMachine.start { (result) in if case let .failure(error) = result { print("Failed to start the virtual machine. \(error)") exit(EXIT_FAILURE) } // if case .success() = result { // // sleep(30) // // socketDevice.connect(toPort: 80) { result in // dump(result) // } // } } } RunLoop.main.run(until: Date.distantFuture) // MARK: - Virtual Machine Delegate func shell(_ command: String) -> String { let task = Process() let pipe = Pipe() task.standardOutput = pipe task.standardError = pipe task.arguments = ["-c", command] task.launchPath = "/bin/zsh" task.standardInput = nil task.launch() let data = pipe.fileHandleForReading.readDataToEndOfFile() let output = String(data: data, encoding: .utf8)! return output } func background(delay: Double = 0.0, background: (()->Void)? = nil, completion: (() -> Void)? = nil) { DispatchQueue.global(qos: .background).async { background?() if let completion = completion { DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: { completion() }) } } } } else { print("only available on macos 13 or later") }