281 lines
10 KiB
Swift
281 lines
10 KiB
Swift
/*
|
||
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")
|
||
}
|