msg-avf/LinuxVirtualMachine/main.swift
2023-09-16 10:08:05 -06:00

281 lines
10 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
See LICENSE folder for this samples 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")
}