initial commit
This commit is contained in:
commit
bccff31856
11 changed files with 876 additions and 0 deletions
94
LinuxVirtualMachine/FileDownloader.swift
Normal file
94
LinuxVirtualMachine/FileDownloader.swift
Normal file
|
@ -0,0 +1,94 @@
|
|||
import Foundation
|
||||
|
||||
class FileDownloader {
|
||||
|
||||
static func loadFileSync(url: URL, completion: @escaping (String?, Error?) -> Void)
|
||||
{
|
||||
if #available(macOS 13.0, *) {
|
||||
let documentsUrl = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
|
||||
let guixUrl = URL(filePath: NSHomeDirectory() + "/.guix/guix.bundle/guix.tar.gz")
|
||||
|
||||
let destinationUrl = guixUrl
|
||||
|
||||
if FileManager().fileExists(atPath: destinationUrl.path)
|
||||
{
|
||||
print("File already exists [\(destinationUrl.path)]")
|
||||
completion(destinationUrl.path, nil)
|
||||
}
|
||||
else if let dataFromURL = NSData(contentsOf: url)
|
||||
{
|
||||
if dataFromURL.write(to: destinationUrl, atomically: true)
|
||||
{
|
||||
print("file saved [\(destinationUrl.path)]")
|
||||
completion(destinationUrl.path, nil)
|
||||
}
|
||||
else
|
||||
{
|
||||
print("error saving file")
|
||||
let error = NSError(domain:"Error saving file", code:1001, userInfo:nil)
|
||||
completion(destinationUrl.path, error)
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
let error = NSError(domain:"Error downloading file", code:1002, userInfo:nil)
|
||||
completion(destinationUrl.path, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static func loadFileAsync(url: URL, completion: @escaping (String?, Error?) -> Void)
|
||||
{
|
||||
if #available(macOS 13.0, *) {
|
||||
|
||||
let documentsUrl = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
|
||||
|
||||
let destinationUrl = documentsUrl.appendingPathComponent(url.lastPathComponent)
|
||||
|
||||
if FileManager().fileExists(atPath: destinationUrl.path)
|
||||
{
|
||||
print("File already exists [\(destinationUrl.path)]")
|
||||
completion(destinationUrl.path, nil)
|
||||
}
|
||||
else
|
||||
{
|
||||
let session = URLSession(configuration: URLSessionConfiguration.default, delegate: nil, delegateQueue: nil)
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "GET"
|
||||
let task = session.dataTask(with: request, completionHandler:
|
||||
{
|
||||
data, response, error in
|
||||
if error == nil
|
||||
{
|
||||
if let response = response as? HTTPURLResponse
|
||||
{
|
||||
if response.statusCode == 200
|
||||
{
|
||||
if let data = data
|
||||
{
|
||||
if let _ = try? data.write(to: destinationUrl, options: Data.WritingOptions.atomic)
|
||||
{
|
||||
completion(destinationUrl.path, error)
|
||||
}
|
||||
else
|
||||
{
|
||||
completion(destinationUrl.path, error)
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
completion(destinationUrl.path, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
completion(destinationUrl.path, error)
|
||||
}
|
||||
})
|
||||
task.resume()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
8
LinuxVirtualMachine/LinuxVirtualMachine.entitlements
Normal file
8
LinuxVirtualMachine/LinuxVirtualMachine.entitlements
Normal file
|
@ -0,0 +1,8 @@
|
|||
<?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>
|
281
LinuxVirtualMachine/main.swift
Normal file
281
LinuxVirtualMachine/main.swift
Normal file
|
@ -0,0 +1,281 @@
|
|||
/*
|
||||
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")
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue