msg-avf/LinuxVirtualMachine/main.swift

282 lines
10 KiB
Swift
Raw Normal View History

2023-09-16 10:08:05 -06:00
/*
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")
}