(allow network-outbound)
diff --git a/Platform/macOS/macOS.entitlements b/Platform/macOS/macOS.entitlements
index 4b3bdf542..de00e8fc0 100644
--- a/Platform/macOS/macOS.entitlements
+++ b/Platform/macOS/macOS.entitlements
@@ -16,6 +16,8 @@
com.apple.security.network.client
+ com.apple.security.network.server
+
com.apple.security.virtualization
com.apple.vm.device-access
diff --git a/Platform/pl.lproj/Localizable.strings b/Platform/pl.lproj/Localizable.strings
index 2a314c61f..b23c0b2a1 100644
--- a/Platform/pl.lproj/Localizable.strings
+++ b/Platform/pl.lproj/Localizable.strings
@@ -73,6 +73,7 @@
"TCP Client Connection" = "Połączenie kilent TCP";
"TCP Server Connection" = "Połączenie serwer TCP";
"Automatic Serial Device (max 4)" = "Automatyczne urządzenie szeregowe (maks. 4)";
+"Automatic" = "Automatyczny";
"Manual Serial Device (advanced)" = "Manualne urządzenie szeregowe (zaawansowane)";
"GDB Debug Stub" = "GDB Debug Stub";
"QEMU Monitor (HMP)" = "Monitor QEMU (HMP)";
@@ -178,7 +179,7 @@
"Information" = "Informacje";
"System" = "System";
"QEMU" = "QEMU";
-"Input" = "Urządzenie WE/WY";
+"Input" = "Urządzenie peryferyjne";
"Sharing" = "Współdzielenie";
"Devices" = "Urządzenia";
"Display" = "Monitor";
@@ -295,10 +296,12 @@
"Sound Backend" = "Tryb dźwięku";
"SPICE with GStreamer (Input & Output)" = "SPICE z GStreamerem (WE/WY)";
"CoreAudio (Output Only)" = "CoreAudio (tylko wyjście)";
+"Mouse/Keyboard" = "Klawiatura/mysz";
+"Capture input automatically when entering full screen" = "Przechwytuj mysz automatycznie w trybie pełnoekranowym";
"Console" = "Konsola";
"Option (⌥) is Meta key" = "Option(⌥) to klawisz Meta";
"If enabled, Option will be mapped to the Meta key which can be useful for emacs. Otherwise, option will work as the system intended (such as for entering international text)." = "Jeśli włączone, klawisz Option będzie zmapowany jako klawisz Meta, który może być przydatny dla eMacków. W innym wypadku, ta opcja będzie działać jak system przewiduje (tj: wpisywanie międzynarodowego tekstu).";
-"QEMU Pointer" = "Wskaźnik QEMU";
+"QEMU Pointer" = "Mysz QEMU";
"Hold Control (⌃) for right click" = "Przytrzymaj Control (⌃) aby wykonać prawe kliknięcie";
"Invert scrolling" = "Odwróć przewijanie";
"If enabled, scroll wheel input will be inverted." = "Jeśli włączone, przewijanie będzie odwrócone";
@@ -517,7 +520,8 @@
"Guest Network (IPv6)" = "Sieć gościa (IPv6)";
"Host Address" = "Adres hosta";
"Host Address (IPv6)" = "Adres hosta(IPv6)";
-"DHCP Start" = "Start DHCP";
+"DHCP Start" = "Pierwszy adres zakresu DHCP";
+"DHCP End" = "Ostatni adres zakresu DHCP";
"DHCP Domain Name" = "Nazwa domeny DHCP";
"DNS Server" = "Serwer DNS";
"DNS Server (IPv6)" = "Serwer DNS(IPv6)";
@@ -550,7 +554,7 @@
"Instantiate PS/2 controller even when USB input is supported. Required for older Windows." = "Utwórz instancję kontrolera PS/2 nawet jeśli kontroler USB jest wspierany. Wymagane dla starszych wersji systemu Windows.";
"QEMU Machine Properties" = "Właściwości maszyny QEMU";
"This is appended to the -machine argument." = "To jest dołączone do argumentu -machine.";
-"QEMU Arguments" = "Argumenty dla QEMU";
+"QEMU Arguments" = "Argumenty rozruchu QEMU";
"Export QEMU Command…" = "Eksportuj komendę QEMU…";
"(Delete)" = "(Usuń)";
diff --git a/Platform/visionOS/UTMApp.swift b/Platform/visionOS/UTMApp.swift
index e3e1487fa..c82496e36 100644
--- a/Platform/visionOS/UTMApp.swift
+++ b/Platform/visionOS/UTMApp.swift
@@ -15,23 +15,40 @@
//
import SwiftUI
+import VisionKeyboardKit
@MainActor
struct UTMApp: App {
+ #if WITH_REMOTE
+ @State private var data: UTMRemoteData = UTMRemoteData()
+ #else
@State private var data: UTMData = UTMData()
+ #endif
@Environment(\.openWindow) private var openWindow
@Environment(\.dismissWindow) private var dismissWindow
private let vmSessionCreatedNotification = NotificationCenter.default.publisher(for: .vmSessionCreated)
private let vmSessionEndedNotification = NotificationCenter.default.publisher(for: .vmSessionEnded)
+ private var contentView: some View {
+ #if WITH_REMOTE
+ RemoteContentView(remoteClientState: data.remoteClient.state)
+ #else
+ ContentView()
+ #endif
+ }
+
var body: some Scene {
WindowGroup(id: "home") {
- ContentView()
+ contentView
.environmentObject(data)
.onReceive(vmSessionCreatedNotification) { output in
let newSession = output.userInfo!["Session"] as! VMSessionState
- openWindow(value: newSession.newWindow())
+ if let window = newSession.windows.first {
+ openWindow(value: window)
+ } else {
+ openWindow(value: newSession.newWindow())
+ }
}
.onReceive(vmSessionEndedNotification) { output in
let endedSession = output.userInfo!["Session"] as! VMSessionState
@@ -46,12 +63,17 @@ struct UTMApp: App {
WindowGroup(for: VMSessionState.GlobalWindowID.self) { $globalID in
if let globalID = globalID, let session = VMSessionState.allActiveSessions[globalID.sessionID] {
VMWindowView(id: globalID.windowID).environmentObject(session)
+ .glassBackgroundEffect(in: .rect(cornerRadius: 15))
+ #if WITH_SOLO_VM
.onAppear {
// currently we only support one session, so close the home window
dismissWindow(id: "home")
}
+ #endif
}
}
+ .windowStyle(.plain)
.windowResizability(.contentMinSize)
+ KeyboardWindowGroup()
}
}
diff --git a/Platform/visionOS/VMToolbarOrnamentModifier.swift b/Platform/visionOS/VMToolbarOrnamentModifier.swift
index bc058a9de..372432cd2 100644
--- a/Platform/visionOS/VMToolbarOrnamentModifier.swift
+++ b/Platform/visionOS/VMToolbarOrnamentModifier.swift
@@ -15,23 +15,35 @@
//
import SwiftUI
+import VisionKeyboardKit
+#if !WITH_USB
+import CocoaSpiceNoUsb
+#else
+import CocoaSpice
+#endif
struct VMToolbarOrnamentModifier: ViewModifier {
@Binding var state: VMWindowState
@EnvironmentObject private var session: VMSessionState
@AppStorage("ToolbarIsCollapsed") private var isCollapsed: Bool = false
+ @Environment(\.openWindow) private var openWindow
+ @Environment(\.dismissWindow) private var dismissWindow
func body(content: Content) -> some View {
content.ornament(visibility: isCollapsed ? .hidden : .visible, attachmentAnchor: .scene(.top)) {
HStack {
Button {
- if session.vm.state == .started {
+ if state.isRunning {
state.alert = .powerDown
} else {
state.alert = .terminateApp
}
} label: {
- Label(state.isRunning ? "Power Off" : "Quit", systemImage: state.isRunning ? "power" : "xmark")
+ if state.isRunning {
+ Label("Power Off", systemImage: "power")
+ } else {
+ Label("Force Kill", systemImage: "xmark")
+ }
}
.disabled(state.isBusy)
Button {
@@ -56,7 +68,7 @@ struct VMToolbarOrnamentModifier: ViewModifier {
}
.disabled(state.isBusy)
}
- #if !WITH_QEMU_TCI
+ #if WITH_USB
if session.vm.hasUsbRedirection {
VMToolbarUSBMenuView()
.disabled(state.isBusy)
@@ -67,11 +79,39 @@ struct VMToolbarOrnamentModifier: ViewModifier {
VMToolbarDisplayMenuView(state: $state)
.disabled(state.isBusy)
Button {
- state.isKeyboardRequested = true
+ if case .display(_, _) = state.device {
+ state.isKeyboardRequested = !state.isKeyboardShown
+ } else {
+ state.isKeyboardRequested = true
+ }
} label: {
Label("Keyboard", systemImage: "keyboard")
}
.disabled(state.isBusy)
+ .onChange(of: state.isKeyboardRequested) { _, newValue in
+ guard case .display(_, _) = state.device else {
+ return
+ }
+ if newValue {
+ openWindow(keyboardFor: state.id)
+ } else {
+ dismissWindow(keyboardFor: state.id)
+ }
+ }
+ .onReceive(KeyboardEvent.publisher(for: state.id)) { event in
+ switch event {
+ case .keyboardDidAppear:
+ state.isKeyboardShown = true
+ state.isKeyboardRequested = true
+ case .keyboardDidDisappear:
+ state.isKeyboardShown = false
+ state.isKeyboardRequested = false
+ case .keyUp(let keyCode, let modifier):
+ handleKeyEvent(keyCode, modifier: modifier, isKeyDown: false)
+ case .keyDown(let keyCode, let modifier):
+ handleKeyEvent(keyCode, modifier: modifier, isKeyDown: true)
+ }
+ }
Divider()
Button {
isCollapsed = true
@@ -90,6 +130,30 @@ struct VMToolbarOrnamentModifier: ViewModifier {
.modifier(ToolbarOrnamentViewModifier())
}
}
+
+ private func handleKeyEvent(_ keyCode: KeyboardKeyCode, modifier: KeyboardModifier, isKeyDown: Bool) {
+ guard let primaryInput = session.primaryInput else {
+ logger.debug("ignoring key event because input channel is not ready")
+ return
+ }
+ var scanCode = keyCode.ps2Set1ScanMake(modifier).reduce(Int32(0), { ($0 << 8) | Int32($1) })
+ if ((scanCode & 0xFF00) == 0xE000) {
+ scanCode = 0x100 | (scanCode & 0xFF);
+ }
+ primaryInput.send(isKeyDown ? .press : .release, code: scanCode)
+ }
+}
+
+// the following was suggested by Apple via Feedback to look close to .toolbar() with .bottomOrnament
+private struct ToolbarOrnamentViewModifier: ViewModifier {
+ func body(content: Content) -> some View {
+ content
+ .buttonBorderShape(.capsule)
+ .buttonStyle(.borderless)
+ .labelStyle(.iconOnly)
+ .padding(12)
+ .glassBackgroundEffect()
+ }
}
// the following was suggested by Apple via Feedback to look close to .toolbar() with .bottomOrnament
diff --git a/Platform/zh-HK.lproj/Localizable.strings b/Platform/zh-HK.lproj/Localizable.strings
index 29f65d128..f2bd35fc3 100644
--- a/Platform/zh-HK.lproj/Localizable.strings
+++ b/Platform/zh-HK.lproj/Localizable.strings
@@ -78,7 +78,7 @@
"An internal error has occurred." = "發生內部錯誤。";
/* UTMConfiguration */
-"An invalid value of '%@' is used in the configuration file." = "設定檔裡使用了無效值「%@」。";
+"An invalid value of '%@' is used in the configuration file." = "設定檔內使用了無效值「%@」。";
/* UTMQemuImage */
"An unknown QEMU error has occurred." = "發生未知的 QEMU 錯誤。";
@@ -90,7 +90,7 @@
"ANGLE (OpenGL)" = "ANGLE (OpenGL)";
/* VMConfigSystemView */
-"Any unsaved changes will be lost." = "任何未儲存的變更都將丟失。";
+"Any unsaved changes will be lost." = "任何未儲存的變更都將遺失。";
/* No comment provided by engineer. */
"Architecture" = "體系結構";
@@ -102,10 +102,10 @@
"Are you sure you want to permanently delete this disk image?" = "確定要永久刪除此磁碟映像檔嗎?";
/* No comment provided by engineer. */
-"Are you sure you want to reset this VM? Any unsaved changes will be lost." = "確定要重設此虛擬電腦嗎?任何未儲存的變更都將丟失。";
+"Are you sure you want to reset this VM? Any unsaved changes will be lost." = "確定要重設此虛擬電腦嗎?任何未儲存的變更都將遺失。";
/* No comment provided by engineer. */
-"Are you sure you want to stop this VM and exit? Any unsaved changes will be lost." = "確定要停止此虛擬電腦並退出嗎?任何未儲存的變更都將丟失。";
+"Are you sure you want to stop this VM and exit? Any unsaved changes will be lost." = "確定要停止此虛擬電腦並結束嗎?任何未儲存的變更都將遺失。";
/* No comment provided by engineer. */
"Automatic" = "自動";
@@ -154,7 +154,7 @@
"Build" = "構建";
/* UTMQemuConstants */
-"Built-in Terminal" = "內置終端機";
+"Built-in Terminal" = "預置終端機";
/* No comment provided by engineer. */
"Busy…" = "正忙⋯";
@@ -182,13 +182,13 @@
"Caps Lock (⇪) is treated as a key" = "將 Caps Lock (⇪) 視為按鍵";
/* VMMetalView */
-"Capture Input" = "截取輸入";
+"Capture Input" = "擷取輸入";
/* No comment provided by engineer. */
-"Capture input automatically when entering full screen" = "進入全螢幕時自動截取輸入";
+"Capture input automatically when entering full screen" = "進入全螢幕時自動擷取輸入";
/* VMDisplayQemuMetalWindowController */
-"Captured mouse" = "已截取滑鼠";
+"Captured mouse" = "已擷取滑鼠";
/* Configuration boot device */
"CD/DVD" = "CD/DVD";
@@ -215,7 +215,8 @@
/* No comment provided by engineer. */
"Confirm Delete" = "確認刪除";
-/* VMDisplayWindowController */
+/* AppDelegate
+ VMDisplayWindowController */
"Confirmation" = "確認";
/* No comment provided by engineer. */
@@ -268,10 +269,10 @@
"Devices" = "裝置";
/* VMDisplayAppleWindowController */
-"Directory sharing" = "目錄共享";
+"Directory sharing" = "目錄分享";
/* UTMQemuConstants */
-"Disabled" = "禁用";
+"Disabled" = "停用";
/* UTMLegacyQemuConfiguration
UTMQemuConstants */
@@ -287,16 +288,16 @@
"Disposable Mode" = "即拋式模式";
/* No comment provided by engineer. */
-"Do not save VM screenshot to disk" = "不要將虛擬電腦快照儲存到磁碟";
+"Do not save VM screenshot to disk" = "不要將虛擬電腦快照儲存至磁碟";
/* No comment provided by engineer. */
-"Do not show confirmation when closing a running VM" = "關閉正在執行的虛擬電腦時不顯示確認";
+"Do not show confirmation when closing a running VM" = "關閉正在執行的虛擬電腦時不要顯示確認";
/* No comment provided by engineer. */
-"Do not show prompt when USB device is plugged in" = "插入 USB 裝置時不顯示提示";
+"Do not show prompt when USB device is plugged in" = "插入 USB 裝置時不要顯示提示";
/* No comment provided by engineer. */
-"Do you want to copy this VM and all its data to internal storage?" = "你要複製此虛擬電腦及其所有資料到內部儲存空間嗎?";
+"Do you want to copy this VM and all its data to internal storage?" = "你要複製此虛擬電腦及其所有資料至內部儲存空間嗎?";
/* No comment provided by engineer. */
"Do you want to delete this VM and all its data?" = "你要刪除此虛擬電腦及其所有資料嗎?";
@@ -305,10 +306,10 @@
"Do you want to duplicate this VM and all its data?" = "你要製作此虛擬電腦及其所有資料的副本嗎?";
/* No comment provided by engineer. */
-"Do you want to force stop this VM and lose all unsaved data?" = "你要強制停止此虛擬電腦並丟失所有未儲存的資料嗎?";
+"Do you want to force stop this VM and lose all unsaved data?" = "你要強行停止此虛擬電腦並遺失所有未儲存的資料嗎?";
/* No comment provided by engineer. */
-"Do you want to move this VM to another location? This will copy the data to the new location, delete the data from the original location, and then create a shortcut." = "你要將此虛擬電腦移動到其他位置嗎?這將會複製資料到新位置,刪除原先位置資料,並製作捷徑。";
+"Do you want to move this VM to another location? This will copy the data to the new location, delete the data from the original location, and then create a shortcut." = "你要將此虛擬電腦移動至其他位置嗎?這將會複製資料至新位置,刪除原先位置資料,並製作捷徑。";
/* No comment provided by engineer. */
"Do you want to remove this shortcut? The data will not be deleted." = "你要刪除此捷徑嗎?資料不會被刪除。";
@@ -317,7 +318,7 @@
"Download prebuilt from UTM Gallery…" = "從 UTM 虛擬電腦庫下載預構建⋯";
/* No comment provided by engineer. */
-"Drag and drop IPSW file here" = "拖移 IPSW 檔到此";
+"Drag and drop IPSW file here" = "拖放 IPSW 檔至此";
/* UTMScriptingConfigImpl */
"Drive description is invalid." = "磁碟描述無效。";
@@ -332,19 +333,19 @@
"Eject" = "退出";
/* No comment provided by engineer. */
-"Emulate" = "模擬";
+"Emulate" = "仿真";
/* UTMQemuConstants */
-"Emulated VLAN" = "模擬 VLAN";
+"Emulated VLAN" = "仿真 VLAN";
/* No comment provided by engineer. */
-"Enable Clipboard Sharing" = "啟用剪貼板共享";
+"Enable Clipboard Sharing" = "啟用剪貼板分享";
/* VMDisplayWindowController */
"Error" = "錯誤";
/* No comment provided by engineer. */
-"Existing" = "已經存在";
+"Existing" = "現存";
/* No comment provided by engineer. */
"Export QEMU Command…" = "輸出 QEMU 指令⋯";
@@ -353,13 +354,13 @@
"Extracting…" = "正在解壓縮⋯";
/* UTMQemuVirtualMachine */
-"Failed to access data from shortcut." = "無法從捷徑取用資料。";
+"Failed to access data from shortcut." = "無法由捷徑取用資料。";
/* UTMQemuVirtualMachine */
"Failed to access drive image path." = "無法取用磁碟映像檔路徑。";
/* UTMQemuVirtualMachine */
-"Failed to access shared directory." = "無法取用共享目錄。";
+"Failed to access shared directory." = "無法取用分享目錄。";
/* ContentView */
"Failed to attach to JitStreamer:\n%@" = "無法附加至 JitStreamer:%@";
@@ -371,16 +372,16 @@
"Failed to change current directory." = "無法變更當前目錄。";
/* UTMData */
-"Failed to clone VM." = "無法克隆虛擬電腦。";
+"Failed to clone VM." = "無法複製虛擬電腦。";
/* UTMData */
"Failed to decode JitStreamer response." = "無法解碼 JitStreamer 回應。";
/* VMWizardState */
-"Failed to get latest macOS version from Apple." = "無法從 Apple 取得最新的 macOS 版本。";
+"Failed to get latest macOS version from Apple." = "無法由 Apple 取得最新的 macOS 版本。";
/* UTMQemuConfigurationError */
-"Failed to migrate configuration from a previous UTM version." = "無法從以前版本的 UTM 轉移設定。";
+"Failed to migrate configuration from a previous UTM version." = "無法由之前版本的 UTM 轉移設定。";
/* UTMData */
"Failed to parse download URL." = "無法解析已經下載的 URL。";
@@ -391,11 +392,12 @@
/* UTMDownloadVMTask */
"Failed to parse the downloaded VM." = "無法解析已經下載的虛擬電腦。";
-/* VMDisplayWindowController */
+/* AppDelegate
+ VMDisplayWindowController */
"Failed to save suspend state" = "無法儲存暫停狀態。";
/* UTMQemuVirtualMachine */
-"Failed to save VM snapshot. Usually this means at least one device does not support snapshots. %@" = "無法儲存虛擬電腦快照。通常這意味著至少有一個裝置不支援快照。%@";
+"Failed to save VM snapshot. Usually this means at least one device does not support snapshots. %@" = "無法儲存虛擬電腦快照。這通常意味著至少有一個裝置不支援快照。%@";
/* UTMSpiceIO */
"Failed to start SPICE client." = "無法啟動 SPICE 客戶端。";
@@ -411,16 +413,16 @@
"Font Size" = "字體大小";
/* VMDisplayWindowController */
-"Force kill" = "強制結束";
+"Force kill" = "強行結束";
/* VMDisplayWindowController */
-"Force kill the VM process with high risk of data corruption." = "強制結束虛擬電腦程序 (會有高風險使資料損壞)。";
+"Force kill the VM process with high risk of data corruption." = "強行結束虛擬電腦程序 (會有高風險使資料損毀)。";
/* No comment provided by engineer. */
-"Force Multicore" = "強制多核心";
+"Force Multicore" = "強行多核心";
/* VMDisplayWindowController */
-"Force shut down" = "強制關機";
+"Force shut down" = "強行關機";
/* No comment provided by engineer. */
"GB" = "GB";
@@ -431,6 +433,12 @@
/* No comment provided by engineer. */
"Generic" = "一般";
+/* UTMAppleConfigurationDevices */
+"Generic Mouse" = "一般滑鼠";
+
+/* UTMAppleConfigurationDevices */
+"Generic USB" = "一般 USB";
+
/* No comment provided by engineer. */
"Gesture and Cursor Settings" = "手勢與指標設定";
@@ -492,7 +500,7 @@
"Internal error has occurred." = "發生內部錯誤。";
/* UTMSpiceIO */
-"Internal error trying to connect to SPICE server." = "在連線 SPICE 伺服器時發生內部錯誤。";
+"Internal error trying to connect to SPICE server." = "在連接 SPICE 伺服器時發生內部錯誤。";
/* VMDisplayMetalWindowController */
"Internal error." = "內部錯誤。";
@@ -557,6 +565,12 @@
/* No comment provided by engineer. */
"Logging" = "日誌";
+/* UTMAppleConfigurationDevices */
+"Mac Keyboard (macOS 14+)" = "Mac 鍵盤 (macOS 14+)";
+
+/* UTMAppleConfigurationDevices */
+"Mac Trackpad (macOS 13+)" = "Mac 觸控板 (macOS 13+)";
+
/* UTMAppleConfigurationBoot */
"macOS" = "macOS";
@@ -573,7 +587,7 @@
"Manual Serial Device (advanced)" = "手動序列裝置 (進階)";
/* No comment provided by engineer. */
-"Maximum Shared USB Devices" = "最大共享 USB 裝置數";
+"Maximum Shared USB Devices" = "最多分享 USB 裝置";
/* No comment provided by engineer. */
"MB" = "MB";
@@ -587,9 +601,6 @@
/* No comment provided by engineer. */
"Minimum size: %@" = "最小大小:%@";
-/* UTMAppleConfigurationDevices */
-"Mouse" = "滑鼠";
-
/* No comment provided by engineer. */
"Mouse/Keyboard" = "滑鼠/鍵盤";
@@ -618,22 +629,22 @@
"No" = "否";
/* UTMScriptingAppDelegate */
-"No architecture specified in the configuration." = "未在設定裡指定體系結構。";
+"No architecture specified in the configuration." = "設定內未指定體系結構。";
/* VMDisplayWindowController */
-"No drives connected." = "無已經連線的磁碟。";
+"No drives connected." = "未有已經連接的磁碟。";
/* UTMDownloadSupportToolsTaskError */
-"No empty removable drive found. Make sure you have at least one removable drive that is not in use." = "無法找到空的可移除式磁碟。請確定你至少有一個未使用的可移除式磁碟。";
+"No empty removable drive found. Make sure you have at least one removable drive that is not in use." = "無法找到空的可移除式磁碟。確保你至少有一個未使用的可移除式磁碟。";
/* UTMScriptingAppDelegate */
-"No name specified in the configuration." = "設定裡沒有指定名稱。";
+"No name specified in the configuration." = "設定內未指定名稱。";
/* No comment provided by engineer. */
"No output device is selected for this window." = "在此視窗內未選取任何輸出裝置。";
/* No comment provided by engineer. */
-"No release notes found for version %@." = "無法找到版本 %@ 的版本附註。";
+"No release notes found for version %@." = "無法找到版本 %@ 的發行註記。";
/* VMQemuDisplayMetalWindowController */
"No USB devices detected." = "未偵測到 USB 裝置。";
@@ -655,10 +666,10 @@
"Not implemented." = "此功能未實現。";
/* No comment provided by engineer. */
-"Notes" = "注意";
+"Notes" = "備註";
/* No comment provided by engineer. */
-"Num Lock is forced on" = "Num Lock 強制開啟";
+"Num Lock is forced on" = "Num Lock 強行開啟";
/* UTMQemuConstants */
"NVMe" = "NVMe";
@@ -724,7 +735,7 @@
"Preconfigured" = "預設定";
/* A download process is about to begin. */
-"Preparing…" = "正在準備...";
+"Preparing…" = "正在準備⋯";
/* VMDisplayQemuMetalWindowController */
"Press %@ to release cursor" = "按下 %@ 來放開指標";
@@ -805,7 +816,7 @@
"Resize display to window size automatically" = "自動將顯示大小調整為視窗大小";
/* No comment provided by engineer. */
-"Resizing is experimental and could result in data loss. You are strongly encouraged to back-up this VM before proceeding. Would you like to resize to %@ GiB?" = "調整空間大小屬於實驗性功能,可能會導致資料丟失。強烈建議你先備份此虛擬電腦,然後再繼續操作。你要調整大小為 %@ GB 嗎?";
+"Resizing is experimental and could result in data loss. You are strongly encouraged to back-up this VM before proceeding. Would you like to resize to %@ GiB?" = "調整空間大小屬於實驗性功能,可能會導致資料遺失。強烈建議你先備份此虛擬電腦,然後再繼續操作。你要調整大小為 %@ GB 嗎?";
/* VMData */
"Restoring" = "正在還原";
@@ -845,7 +856,7 @@
/* VMDisplayAppleWindowController
VMDisplayWindowController */
-"Select Shared Folder" = "選取共享的資料夾";
+"Select Shared Folder" = "選取分享的資料夾";
/* SavePanel */
"Select where to export QEMU command:" = "選取輸出 QEMU 指令的位置:";
@@ -860,26 +871,26 @@
"Selected:" = "已選取:";
/* VMDisplayWindowController */
-"Sends power down request to the guest. This simulates pressing the power button on a PC." = "向客戶端發送關閉電源請求。此操作模擬了按下 PC 上的電源按鈕。";
+"Sends power down request to the guest. This simulates pressing the power button on a PC." = "向客戶端發送關閉電源請求。此操作仿真了按下 PC 上的電源按鈕。";
/* VMDisplayAppleWindowController
VMDisplayQemuDisplayController */
"Serial %lld" = "序列裝置 %lld";
/* No comment provided by engineer. */
-"Share USB devices from host" = "從主機共享 USB 裝置";
+"Share USB devices from host" = "從主機分享 USB 裝置";
/* No comment provided by engineer. */
-"Shared directories in macOS VMs are only available in macOS 13 and later." = "macOS 虛擬電腦共享目錄僅在 macOS 13 及更高版本可用。";
+"Shared directories in macOS VMs are only available in macOS 13 and later." = "macOS 虛擬電腦分享目錄僅在 macOS 13 及更高版本可用。";
/* No comment provided by engineer. */
-"Shared Directory" = "已共享目錄";
+"Shared Directory" = "已分享目錄";
/* UTMQemuConstants */
-"Shared Network" = "共享網絡";
+"Shared Network" = "已分享網絡";
/* No comment provided by engineer. */
-"Sharing" = "共享";
+"Sharing" = "分享";
/* No comment provided by engineer. */
"Show Advanced Settings" = "顯示進階設定";
@@ -906,7 +917,7 @@
"Socket not specified." = "未指定 socket。";
/* No comment provided by engineer. */
-"Specify the size of the drive where data will be stored into." = "指定資料儲存到的磁碟的大小。";
+"Specify the size of the drive where data will be stored into." = "指定資料儲存至的磁碟的大小。";
/* UTMQemuConstants */
"SPICE WebDAV" = "SPICE WebDAV";
@@ -942,7 +953,7 @@
"Suspend is not supported for virtualization." = "暫停功能不支援虛擬化。";
/* UTMQemuVirtualMachine */
-"Suspend is not supported when an emulated NVMe device is active." = "当模擬 NVMe 裝置處於啟用狀態時,不支援暫停功能。";
+"Suspend is not supported when an emulated NVMe device is active." = "当仿真 NVMe 裝置處於啟用狀態時,不支援暫停功能。";
/* UTMQemuVirtualMachine */
"Suspend is not supported when GPU acceleration is enabled." = "当 GPU 加速處於啟用狀態時,不支援暫停功能。";
@@ -969,7 +980,7 @@
"TCP Server Connection" = "TCP 伺服器連線";
/* VMDisplayWindowController */
-"Tells the VM process to shut down with risk of data corruption. This simulates holding down the power button on a PC." = "告知關閉虛擬電腦程序(有損毀資料的風險)。此操作模擬了按下 PC 上的電源按鈕。";
+"Tells the VM process to shut down with risk of data corruption. This simulates holding down the power button on a PC." = "告知虛擬電腦程序關閉,並有損毀資料的風險。此操作仿真了在 PC 上按下電源按鈕。";
/* No comment provided by engineer. */
"Test" = "測試";
@@ -987,7 +998,7 @@
"The device cannot be found." = "無法找到此裝置。";
/* UTMScriptingUSBDeviceImpl */
-"The device is not currently connected." = "此裝置當前未連線。";
+"The device is not currently connected." = "此裝置當前未連接。";
/* UTMConfiguration */
"The drive '%@' already exists and cannot be created." = "磁碟「%@」已經存在,無法製作。";
@@ -1014,7 +1025,7 @@
"The selected boot image contains the word '%@' but the guest architecture is '%@'. Please ensure you have selected an image that is compatible with '%@'." = "選取的啟動映像檔包含單字「%1$@」,但客戶端體系結構為「%2$@」。請確保你選取了與「%3$@」體系結構相容的映像檔。";
/* No comment provided by engineer. */
-"The target does not support hardware emulated serial connections." = "目標平台不支援硬件模擬序列連線。";
+"The target does not support hardware emulated serial connections." = "目標平台不支援硬件仿真序列連線。";
/* UTMQemuVirtualMachine */
"The virtual machine is in an invalid state." = "虛擬電腦處於無效狀態。";
@@ -1026,7 +1037,7 @@
"The virtual machine must be stopped before this operation can be performed." = "必須先停止虛擬電腦,然後才能執行此操作。";
/* Error shown when importing a ZIP file from web that doesn't contain a UTM Virtual Machine. */
-"There is no UTM file in the downloaded ZIP archive." = "在已下載的 ZIP 封存檔裡無 UTM 檔案。";
+"There is no UTM file in the downloaded ZIP archive." = "在已經下載的 ZIP 封存檔內未有 UTM 檔案。";
/* No comment provided by engineer. */
"This audio card is not supported." = "此聲卡不支援。";
@@ -1035,10 +1046,10 @@
"This backend is not supported on your machine." = "你的電腦不支援此後端。";
/* No comment provided by engineer. */
-"This build does not emulation." = "此 UTM 構建不支援模擬。";
+"This build does not emulation." = "此 UTM 構建不支援仿真。";
/* UTMQemuVirtualMachine */
-"This build of UTM does not support emulating the architecture of this VM." = "此 UTM 構建不支援模擬該虛擬電腦的體系結構。";
+"This build of UTM does not support emulating the architecture of this VM." = "此 UTM 構建不支援仿真該虛擬電腦的體系結構。";
/* VMConfigSystemView */
"This change will reset all settings" = "此變更會重設所有設定";
@@ -1053,13 +1064,13 @@
"This device is not supported by the target." = "目標不支援此裝置。";
/* VMConfigAppleSharingView */
-"This directory is already being shared." = "此目錄已經被共享。";
+"This directory is already being shared." = "此目錄已被分享。";
/* UTMAppleConfiguration */
"This is not a valid Apple Virtualization configuration." = "並非有效的 Apple 虛擬化設定。";
/* VMDisplayWindowController */
-"This may corrupt the VM and any unsaved changes will be lost. To quit safely, shut down from the guest." = "這可能會損毀虛擬電腦,任何未儲存的變更都將丟失。如要安全退出,請從客戶端關機。";
+"This may corrupt the VM and any unsaved changes will be lost. To quit safely, shut down from the guest." = "這可能會損毀虛擬電腦,任何未儲存的變更都將遺失。如要安全退出,請從客戶端關機。";
/* No comment provided by engineer. */
"This operating system is unsupported on your machine." = "你的電腦不支援此作業系統。";
@@ -1077,13 +1088,13 @@
"This virtual machine has been removed." = "此虛擬電腦已經被刪除。";
/* VMDisplayWindowController */
-"This will reset the VM and any unsaved state will be lost." = "這將重設虛擬電腦,任何未儲存的狀態都將丟失。";
+"This will reset the VM and any unsaved state will be lost." = "這將重設虛擬電腦,任何未儲存的狀態都將遺失。";
/* VMDisplayAppleWindowController */
-"To access the shared directory, the guest OS must have Virtiofs drivers installed. You can then run `sudo mount -t virtiofs share /path/to/share` to mount to the share path." = "要取用共享目錄,客戶端作業系統必須安裝 VirtioFS 驅動程式。然後,你可以執行「sudo mount -t virtiofs share /path/to/share」來裝載到共享路徑。";
+"To access the shared directory, the guest OS must have Virtiofs drivers installed. You can then run `sudo mount -t virtiofs share /path/to/share` to mount to the share path." = "要取用分享目錄,客戶端作業系統必須安裝 VirtioFS 驅動程式。然後,你可以執行「sudo mount -t virtiofs share /path/to/share」來裝載至分享路徑。";
/* VMMetalView */
-"To capture input or to release the capture, press Command and Option at the same time." = "要截取或放開輸入,請同時按下 Command + Option。";
+"To capture input or to release the capture, press Command and Option at the same time." = "要擷取或放開輸入,請同時按下 Command + Option。";
/* No comment provided by engineer. */
"To install macOS, you need to download a recovery IPSW. If you do not select an existing IPSW, the latest macOS IPSW will be downloaded from Apple." = "如要安裝 macOS,你需要下載 IPSW 恢復檔。如你未選取現有的 IPSW,將從 Apple 下載最新的 macOS IPSW。";
@@ -1091,9 +1102,6 @@
/* VMDisplayQemuMetalWindowController */
"To release the mouse cursor, press %@ at the same time." = "如要放開滑鼠指標,請同時按下 %@。";
-/* UTMAppleConfigurationDevices */
-"Trackpad" = "觸控板";
-
/* No comment provided by engineer. */
"u{2022} " = "u{2022}";
@@ -1137,13 +1145,13 @@
"USB Device" = "USB 裝置";
/* No comment provided by engineer. */
-"USB Sharing" = "USB 共享";
+"USB Sharing" = "USB 分享";
/* No comment provided by engineer. */
-"USB sharing not supported in this build of UTM." = "此 UTM 構建不支援 USB 共享。";
+"USB sharing not supported in this build of UTM." = "此 UTM 構建不支援 USB 分享。";
/* No comment provided by engineer. */
-"Use Command+Option (⌘+⌥) for input capture/release" = "使用 Command + Option (⌘ + ⌥) 來截取/放開輸入";
+"Use Command+Option (⌘+⌥) for input capture/release" = "使用 Command + Option (⌘ + ⌥) 來擷取/放開輸入";
/* Welcome view */
"User Guide" = "用戶指南";
@@ -1181,7 +1189,7 @@
"VM display size is fixed" = "虛擬電腦顯示大小固定";
/* No comment provided by engineer. */
-"Waiting for VM to connect to display..." = "正在等待虛擬電腦連線到顯示...";
+"Waiting for VM to connect to display..." = "正在等待虛擬電腦連接至顯示...";
/* No comment provided by engineer. */
"Welcome to UTM" = "歡迎使用 UTM";
@@ -1193,27 +1201,818 @@
"Windows Guest Support Tools" = "Windows 客戶端支援工具";
/* VMQemuDisplayMetalWindowController */
-"Would you like to connect '%@' to this virtual machine?" = "你要連線「%@」到此虛擬電腦嗎?";
+"Would you like to connect '%@' to this virtual machine?" = "你要連接「%@」至此虛擬電腦嗎?";
/* VMDisplayAppleWindowController */
"Would you like to install macOS? If an existing operating system is already installed on the primary drive of this VM, then it will be erased." = "你要安裝 macOS 嗎?如果此虛擬電腦的主磁碟上已經安裝了現有的作業系統,則會將其清除。";
/* No comment provided by engineer. */
-"Would you like to re-convert this disk image to reclaim unused space and apply compression? Note this will require enough temporary space to perform the conversion. Compression only applies to existing data and new data will still be written uncompressed. You are strongly encouraged to back-up this VM before proceeding." = "你要重新轉換此磁碟映像檔以回收未使用的空間並應用壓縮嗎?請注意,這將需要足夠的臨時空間來執行轉換,壓縮僅適用於現有資料,新資料仍將以未壓縮寫入。強烈建議你先備份此虛擬電腦,然後再繼續操作。";
+"Would you like to re-convert this disk image to reclaim unused space and apply compression? Note this will require enough temporary space to perform the conversion. Compression only applies to existing data and new data will still be written uncompressed. You are strongly encouraged to back-up this VM before proceeding." = "你要重新轉換此磁碟映像檔以回收未使用的空間並應用壓縮嗎?請緊記,這將需要足夠的臨時空間來執行轉換,壓縮僅適用於現有資料,新資料仍將以未壓縮寫入。強烈建議你先備份此虛擬電腦,然後再繼續操作。";
/* No comment provided by engineer. */
-"Would you like to re-convert this disk image to reclaim unused space? Note this will require enough temporary space to perform the conversion. You are strongly encouraged to back-up this VM before proceeding." = "你要重新轉換此磁碟映像檔以回收未使用的空間嗎?請注意,這將需要足夠的臨時空間來執行轉換。強烈建議你先備份此虛擬電腦,然後再繼續操作。";
+"Would you like to re-convert this disk image to reclaim unused space? Note this will require enough temporary space to perform the conversion. You are strongly encouraged to back-up this VM before proceeding." = "你要重新轉換此磁碟映像檔以回收未使用的空間嗎?請緊記,這將需要足夠的臨時空間來執行轉換。強烈建議你先備份此虛擬電腦,然後再繼續操作。";
/* No comment provided by engineer. */
"Yes" = "是";
/* VMConfigSystemView */
-"Your device has %llu MB of memory and the estimated usage is %llu MB." = "你的裝置有 %1$llu MB 的記憶體,大約使用量為 %2$llu MB。";
+"Your device has %llu MB of memory and the estimated usage is %llu MB." = "你的裝置有 %1$llu MB 大小的記憶體,大約使用量為 %2$llu MB。";
/* VMConfigAppleBootView
VMWizardOSMacView */
"Your machine does not support running this IPSW." = "你的電腦不支援執行此 IPSW。";
/* ContentView */
-"Your version of iOS does not support running VMs while unmodified. You must either run UTM while jailbroken or with a remote debugger attached. See https://getutm.app/install/ for more details." = "你的 iOS 版本不支援在未作更動的情況下執行虛擬電腦,必須在越獄 (jailbreak) 時執行 UTM,或是在附加遠程除錯器的情況下執行 UTM。有關更多詳細訊息,請參閲 https://getutm.app/install/。";
+"Your version of iOS does not support running VMs while unmodified. You must either run UTM while jailbroken or with a remote debugger attached. See https://getutm.app/install/ for more details." = "你的 iOS 版本不支援在未作更動的情況下執行虛擬電腦,必須在越獄 (jailbreak) 時執行 UTM,或是在附加遠程除錯器的情況下執行 UTM。有關更多詳細訊息,請見 https://getutm.app/install/。";
+
+// Additonal Strings (Unable to be exported by Xcode)
+
+/* No comment provided by engineer. */
+"(Delete)" = "(刪除)";
+
+/* No comment provided by engineer. */
+"Add" = "加入";
+
+/* No comment provided by engineer. */
+"Add a new device." = "加入一個新裝置。";
+
+/* No comment provided by engineer. */
+"Add a new drive." = "加入一個新磁碟。";
+
+/* No comment provided by engineer. */
+"Add read only" = "加入唯讀";
+
+/* No comment provided by engineer. */
+"Advanced. If checked, a raw disk image is used. Raw disk image does not support snapshots and will not dynamically expand in size." = "進階選項。如選取,將會使用 Raw 磁碟映像。Raw 磁碟映像不支援快照,也不會動態擴充套件大小。";
+
+/* No comment provided by engineer. */
+"Allow Remote Connection" = "允許遠端連線";
+
+/* No comment provided by engineer. */
+"Allows passing through additional input from trackpads. Only supported on macOS 13+ guests." = "允許透過觸控板額外輸入。僅支援 macOS 13+ 客戶端。";
+
+/* No comment provided by engineer. */
+"Apple Virtualization is experimental and only for advanced use cases. Leave unchecked to use QEMU, which is recommended." = "Apple 虛擬化為試驗性質,僅可用作進階用例,不選取此剔選框以使用推介的 QEMU。";
+
+/* No comment provided by engineer. */
+"Application" = "應用程式";
+
+/* No comment provided by engineer. */
+"Architecture" = "體系結構";
+
+/* No comment provided by engineer. */
+"Arguments" = "參數";
+
+/* No comment provided by engineer. */
+"Auto Resolution" = "自動調整解像度";
+
+/* No comment provided by engineer. */
+"Automatic" = "自動";
+
+/* No comment provided by engineer. */
+"Background Color" = "背景顏色";
+
+/* No comment provided by engineer. */
+"Balloon Device" = "Balloon 裝置";
+
+/* No comment provided by engineer. */
+"Blinking cursor?" = "閃爍指標?";
+
+/* No comment provided by engineer. */
+"Boot arguments" = "啟動參數";
+
+/* No comment provided by engineer. */
+"Boot Arguments" = "啟動參數";
+
+/* No comment provided by engineer. */
+"Boot from kernel image" = "由核心映像檔啟動";
+
+/* No comment provided by engineer. */
+"Boot Image" = "啟動映像檔";
+
+/* No comment provided by engineer. */
+"Boot Image Type" = "啟動映像檔類型";
+
+/* No comment provided by engineer. */
+"Boot into recovery mode." = "啟動至還原模式。";
+
+/* No comment provided by engineer. */
+"Bootloader" = "Bootloader";
+
+/* No comment provided by engineer. */
+"Bridged Interface" = "橋連介面";
+
+/* No comment provided by engineer. */
+"Bridged Settings" = "橋連設定";
+
+/* No comment provided by engineer. */
+"By default, the best backend for the target will be used. If the selected backend is not available for any reason, an alternative will automatically be selected." = "於預設情況下,將會使用目標的最好後端。如果所選後端由於任何原因不可用,將會自動選取替代方案。";
+
+/* No comment provided by engineer. */
+"By default, the best renderer for this device will be used. You can override this with to always use a specific renderer. This only applies to QEMU VMs with GPU accelerated graphics." = "於預設情況下,將會使用最適合此裝置的渲染器。你可以覆蓋它以始終使用特定的渲染器,這僅適用於具有 GPU 加速圖形的 QEMU 虛擬電腦。";
+
+/* No comment provided by engineer. */
+"Calculating current size..." = "計算現時大小⋯";
+
+/* No comment provided by engineer. */
+"Cancel Download" = "取消下載";
+
+/* No comment provided by engineer. */
+"Clipboard Sharing" = "剪貼板分享";
+
+/* No comment provided by engineer. */
+"Clone" = "複製";
+
+/* No comment provided by engineer. */
+"Clone selected VM" = "複製已選取的虛擬電腦";
+
+/* No comment provided by engineer. */
+"Clone…" = "複製⋯";
+
+/* No comment provided by engineer. */
+"Close" = "關閉";
+
+/* No comment provided by engineer. */
+"Closing a VM without properly shutting it down could result in data loss." = "在未正確關閉電源的情況下關閉虛擬電腦可能會導致資料遺失。";
+
+/* No comment provided by engineer. */
+"Compress" = "壓縮";
+
+/* No comment provided by engineer. */
+"Compress by re-converting the disk image and compressing the data." = "透過重新轉換磁碟映像檔與壓縮資料來壓縮。";
+
+/* No comment provided by engineer. */
+"Create a new VM" = "製作一個新虛擬電腦";
+
+/* No comment provided by engineer. */
+"Create a new VM with the same configuration as this one but without any data." = "製作一個與此配置相同的新虛擬電腦,但無任何資料。";
+
+/* No comment provided by engineer. */
+"Create an empty drive." = "製作一個空磁碟。";
+
+/* No comment provided by engineer. */
+"Debian Install Guide" = "Debian 安裝指引";
+
+/* No comment provided by engineer. */
+"Default is 1/4 of the RAM size (above). The JIT cache size is additive to the RAM size in the total memory usage!" = "預設值為記憶體大小的 1/4 (見上)。JIT 快取資料大小亦會包括於全部的記憶體使用量當中!";
+
+/* No comment provided by engineer. */
+"Delete this drive." = "刪除此磁碟。";
+
+/* No comment provided by engineer. */
+"Delete selected VM" = "刪除已選取的虛擬電腦";
+
+/* No comment provided by engineer. */
+"Delete this shortcut. The underlying data will not be deleted." = "刪除此捷徑。當中的資料不會被刪除。";
+
+/* No comment provided by engineer. */
+"Delete this VM and all its data." = "刪除此虛擬電腦與其所有資料。";
+
+/* No comment provided by engineer. */
+"Delete Drive" = "刪除磁碟機";
+
+/* No comment provided by engineer. */
+"Description" = "註解";
+
+/* No comment provided by engineer. */
+"Devices" = "裝置";
+
+/* No comment provided by engineer. */
+"Directory" = "目錄";
+
+/* No comment provided by engineer. */
+"Directory Share Mode" = "目錄分享模式";
+
+/* No comment provided by engineer. */
+"Disk" = "磁碟";
+
+/* No comment provided by engineer. */
+"DHCP Domain Name" = "DHCP 網域名稱";
+
+/* No comment provided by engineer. */
+"DHCP End" = "DHCP 結束";
+
+/* No comment provided by engineer. */
+"DNS Search Domains" = "DNS 搜尋網域";
+
+/* No comment provided by engineer. */
+"DNS Server" = "DNS 伺服器";
+
+/* No comment provided by engineer. */
+"DNS Server (IPv6)" = "DNS 伺服器 (IPv6)";
+
+/* No comment provided by engineer. */
+"DHCP Start" = "DHCP 開始";
+
+/* No comment provided by engineer. */
+"Done" = "完成";
+
+/* No comment provided by engineer. */
+"Duplicate this VM along with all its data." = "複製此虛擬電腦及其所有資料。";
+
+/* No comment provided by engineer. */
+"Download and mount the guest support package for Windows. This is required for some features including dynamic resolution and clipboard sharing." = "下載並裝載 Windows 的客戶端支援套件。這對於一些功能為必需,當中包括動態解像度與剪貼板分享。";
+
+/* No comment provided by engineer. */
+"Download and mount the guest tools for Windows." = "下載並裝載 Windows 的客戶端工具。";
+
+/* No comment provided by engineer. */
+"Download Windows 11 for ARM64 Preview VHDX" = "下載 Windows 11 ARM64 Preview VHDX 映像檔";
+
+/* No comment provided by engineer. */
+"Downscaling" = "細化解像度";
+
+/* No comment provided by engineer. */
+"Edit" = "編輯";
+
+/* No comment provided by engineer. */
+"Edit selected VM" = "編輯已選取的虛擬電腦";
+
+/* No comment provided by engineer. */
+"Edit…" = "編輯⋯";
+
+/* No comment provided by engineer. */
+"Emulated Audio Card" = "仿真聲卡";
+
+/* No comment provided by engineer. */
+"Emulated Display Card" = "仿真顯卡";
+
+/* No comment provided by engineer. */
+"Emulated Network Card" = "仿真網卡";
+
+/* No comment provided by engineer. */
+"Emulated Serial Device" = "仿真序列裝置";
+
+/* No comment provided by engineer. */
+"Enable Balloon Device" = "啟用 Balloon 裝置";
+
+/* No comment provided by engineer. */
+"Enable Entropy Device" = "啟用 Entropy 裝置";
+
+/* No comment provided by engineer. */
+"Enable hardware OpenGL acceleration" = "啟用硬體 OpenGL 加速";
+
+/* No comment provided by engineer. */
+"Enable Keyboard" = "啟用鍵盤";
+
+/* No comment provided by engineer. */
+"Enable Pointer" = "啟用指標";
+
+/* No comment provided by engineer. */
+"Enable Rosetta (x86_64 Emulation)" = "啟用 Rosetta (x86_64 仿真)";
+
+/* No comment provided by engineer. */
+"Enable Rosetta on Linux (x86_64 Emulation)" = "於 Linux 中啟用 Rosetta (x86_64 仿真)";
+
+/* No comment provided by engineer. */
+"Enable Sound" = "啟用聲音";
+
+/* No comment provided by engineer. */
+"Engine" = "引擎";
+
+/* No comment provided by engineer. */
+"Export all arguments as a text file. This is only for debugging purposes as UTM's built-in QEMU differs from upstream QEMU in supported arguments." = "將所有參數輸出為一個文字文件。這僅用於除錯目的,因為 UTM 的內建 QEMU 在支援的參數上與上游的 QEMU 不同。";
+
+/* No comment provided by engineer. */
+"Export Debug Log" = "輸出除錯記錄";
+
+/* No comment provided by engineer. */
+"External Drive" = "外部磁碟";
+
+/* No comment provided by engineer. */
+"Fetch latest Windows installer…" = "取得最新的 Windows 安裝工具⋯";
+
+/* No comment provided by engineer. */
+"Font" = "字體";
+
+/* No comment provided by engineer. */
+"Force Disable CPU Flags" = "強制停用 CPU 標記";
+
+/* No comment provided by engineer. */
+"Force Enable CPU Flags" = "強制啟用 CPU 標記";
+
+/* No comment provided by engineer. */
+"Force multicore may improve speed of emulation but also might result in unstable and incorrect emulation." = "強行多核心可能會提高仿真速度,但也會導致不穩定與錯誤的仿真。";
+
+/* No comment provided by engineer. */
+"Force PS/2 controller" = "強行使用 PS/2 控制器";
+
+/* No comment provided by engineer. */
+"FPS Limit" = "FPS 限制";
+
+/* No comment provided by engineer. */
+"Go Back" = "返回";
+
+/* No comment provided by engineer. */
+"GPU Acceleration Supported" = "支援 GPU 加速";
+
+/* No comment provided by engineer. */
+"Guest Address" = "客戶端位址";
+
+/* No comment provided by engineer. */
+"Guest Network" = "客戶端網絡";
+
+/* No comment provided by engineer. */
+"Guest Network (IPv6)" = "客戶端網絡 (IPv6)";
+
+/* No comment provided by engineer. */
+"Guest Port" = "客戶端埠";
+
+/* No comment provided by engineer. */
+"Hardware interface on the guest used to mount this image. Different operating systems support different interfaces. The default will be the most common interface." = "用作裝載此映像的客戶端硬體介面。不同的作業系統支援不同的介面。預設將會設定為最常見的介面。";
+
+/* No comment provided by engineer. */
+"Hardware OpenGL Acceleration" = "硬體 OpenGL 加速";
+
+/* No comment provided by engineer. */
+"Height" = "高度";
+
+/* No comment provided by engineer. */
+"Hide" = "隱藏";
+
+/* No comment provided by engineer. */
+"Hide dock icon on next launch" = "下次啟動時隱藏 Dock 圖示";
+
+/* No comment provided by engineer. */
+"Host Address" = "主機位址";
+
+/* No comment provided by engineer. */
+"Host Address (IPv6)" = "主機位址 (IPv6)";
+
+/* No comment provided by engineer. */
+"Host Port" = "主機埠";
+
+/* No comment provided by engineer. */
+"If checked, no drive image will be stored with the VM. Instead you can mount/unmount image while the VM is running." = "如選取,將不會儲存磁碟映像檔至虛擬電腦內。然而,你可以於虛擬電腦執行時裝載/卸除安裝映像。";
+
+/* No comment provided by engineer. */
+"If checked, the CPU flag will be enabled. Otherwise, the default value will be used." = "如選取,將會啟用此 CPU 標記。否則將會使用預設值。";
+
+/* No comment provided by engineer. */
+"If checked, the CPU flag will be disabled. Otherwise, the default value will be used." = "如選取,將會停用此 CPU 標記。否則將會使用預設值。";
+
+/* No comment provided by engineer. */
+"If checked, the drive image will be stored with the VM." = "如選取,磁碟映像檔將會與虛擬電腦一齊儲存。";
+
+/* No comment provided by engineer. */
+"If checked, use local time for RTC which is required for Windows. Otherwise, use UTC clock." = "如選取,將會使用 Windows 所需要的 RTC 本地時間。否則將會使用 UTC 時鐘。";
+
+/* No comment provided by engineer. */
+"If disabled, the default combination Control+Option (⌃+⌥) will be used." = "如停用,將會使用預設組合鍵 Control + Option (⌃ + ⌥)。";
+
+/* No comment provided by engineer. */
+"If enabled, a virtiofs share tagged 'rosetta' will be available on the Linux guest for installing Rosetta for emulating x86_64 on ARM64." = "如啟用,標記為「rosetta」的 virtiofs 分享將會於 Linux 客戶端上可用,用作安裝 Rosetta,可以於 arm64 上仿真 x86_64。";
+
+/* No comment provided by engineer. */
+"If enabled, any existing screenshot will be deleted the next time the VM is started." = "如啟用,下次啟動虛擬電腦時,任何現存的快照將會被刪除。";
+
+/* No comment provided by engineer. */
+"If enabled, caps lock will be handled like other keys. If disabled, it is treated as a toggle that is synchronized with the host." = "如啟用,Caps Lock 將會同其他鍵一樣處理。如停用,它將會被視為開關鍵,並與主機同步。";
+
+/* No comment provided by engineer. */
+"If enabled, input capture will toggle automatically when entering and exiting full screen mode." = "如啟用,於進入和離開全螢幕模式時,將會自動切換輸入擷取。";
+
+/* No comment provided by engineer. */
+"If enabled, num lock will always be on to the guest. Note this may make your keyboard's num lock indicator out of sync." = "如啟用,Num Lock 將會始終對客戶端開啟。請緊記,這可能會令鍵盤的 Num Lock 指示器不同步。";
+
+/* No comment provided by engineer. */
+"If enabled, Option will be mapped to the Meta key which can be useful for emacs. Otherwise, option will work as the system intended (such as for entering international text)." = "如啟用,Option 鍵將會對映至 Meta 鍵,這對於 Emacs 很有用。否則,Option 鍵將會以系統預設工作(例如輸入國際文字)。";
+
+/* No comment provided by engineer. */
+"If enabled, resizing of the VM window will not be allowed." = "如啟用,將不會允許調整虛擬電腦視窗的大小。";
+
+/* No comment provided by engineer. */
+"If enabled, scroll wheel input will be inverted." = "如啟用,將會反轉滾輪輸入。";
+
+/* No comment provided by engineer. */
+"If enabled, the default input devices will be emulated on the USB bus." = "如啟用,預設輸入裝置將會於 USB 匯流排上仿真。";
+
+/* No comment provided by engineer. */
+"If set, a frame limit can improve smoothness in rendering by preventing stutters when set to the lowest value your device can handle." = "如設定了幀限制,則當設定為你的裝置可以處理的最低值時,幀限制可以防止卡頓,藉此提升渲染的平滑度。";
+
+/* No comment provided by engineer. */
+"If set, boot directly from a raw kernel image and initrd. Otherwise, boot from a supported ISO." = "如設定,直接由 Raw 核心映像檔與 initrd 啟動。否則由受支援的 ISO 啟動。";
+
+/* No comment provided by engineer. */
+"Image Type" = "映像檔類型";
+
+/* No comment provided by engineer. */
+"Import Drive" = "輸入磁碟機";
+
+/* No comment provided by engineer. */
+"Import VHDX Image" = "輸入 VHDX 映像檔";
+
+/* No comment provided by engineer. */
+"Increase the size of the disk image." = "增加磁碟映像檔的大小。";
+
+/* No comment provided by engineer. */
+"Initial Ramdisk" = "初始 ramdisk";
+
+/* No comment provided by engineer. */
+"Input" = "輸入";
+
+/* No comment provided by engineer. */
+"Install drivers and SPICE tools" = "安裝驅動程式與 SPICE 工具";
+
+/* No comment provided by engineer. */
+"Install Windows 10 or higher" = "安裝 Windows 10 或者更高版本";
+
+/* No comment provided by engineer. */
+"Installation Instructions" = "安裝指引";
+
+/* No comment provided by engineer. */
+"Instantiate PS/2 controller even when USB input is supported. Required for older Windows." = "即便支援 USB 輸入,仍然例項化 PS/2 控制器。此項對於舊版 Windows 為必需。";
+
+/* No comment provided by engineer. */
+"Interface" = "介面";
+
+/* No comment provided by engineer. */
+"IPSW Install Image" = "IPSW 安裝映像檔";
+
+/* No comment provided by engineer. */
+"JIT Cache" = "JIT 快取資料";
+
+/* No comment provided by engineer. */
+"Kernel" = "核心";
+
+/* No comment provided by engineer. */
+"Kernel Image" = "核心映像檔";
+
+/* No comment provided by engineer. */
+"Keyboard" = "鍵盤";
+
+/* No comment provided by engineer. */
+"MAC Address" = "MAC 位址";
+
+/* No comment provided by engineer. */
+"Machine" = "電腦";
+
+/* No comment provided by engineer. */
+"Maintenance" = "維護";
+
+/* No comment provided by engineer. */
+"Mode" = "模式";
+
+/* No comment provided by engineer. */
+"Modify settings for this VM." = "修改此虛擬電腦的設定。";
+
+/* UTMAppleConfigurationDevices */
+"Mouse" = "滑鼠";
+
+/* No comment provided by engineer. */
+"Move" = "移動";
+
+/* No comment provided by engineer. */
+"Move…" = "移動⋯";
+
+/* No comment provided by engineer. */
+"Move selected VM" = "移動已選取的虛擬電腦";
+
+/* No comment provided by engineer. */
+"Move this VM from internal storage to elsewhere." = "將此虛擬電腦由內部儲存空間移動至其他地方。";
+
+/* No comment provided by engineer. */
+"Network" = "網絡";
+
+/* No comment provided by engineer. */
+"Network Mode" = "網絡模式";
+
+/* No comment provided by engineer. */
+"New Drive" = "新增磁碟機";
+
+/* No comment provided by engineer. */
+"New from template…" = "由此範本新增⋯";
+
+/* No comment provided by engineer. */
+"New Shared Directory…" = "新增分享目錄⋯";
+
+/* No comment provided by engineer. */
+"New VM" = "新增虛擬電腦";
+
+/* No comment provided by engineer. */
+"Older versions of UTM added each IDE device to a separate bus. Check this to change the configuration to place two units on each bus." = "舊版本的 UTM 將每個 IDE 裝置加至單獨的匯流排中。檢查此項以更改配置,以便於每個匯流排上放置兩個單元。";
+
+/* No comment provided by engineer. */
+"Only available if host architecture matches the target. Otherwise, TCG emulation is used." = "僅當主機架構與目標匹配時才可用。否則,將使用 TCG 仿真。";
+
+/* No comment provided by engineer. */
+"Only available on macOS virtual machines." = "僅可用於 macOS 虛擬電腦。";
+
+/* No comment provided by engineer. */
+"Only available when Hypervisor is used on supported hardware. TSO speeds up Intel emulation in the guest at the cost of decreased performance in general." = "僅當於受支援的硬體上使用 Hypervisor 時才可用。TSO 提升了客戶端的 Intel 仿真速度,但以總體的效能降低為代價。";
+
+/* No comment provided by engineer. */
+"Open VM Settings" = "開啟虛擬電腦設定";
+
+/* No comment provided by engineer. */
+"Optionally select a directory to make accessible inside the VM. Note that support for shared directories varies by the guest operating system and may require additional guest drivers to be installed. See UTM support pages for more details." = "(可選) 選取一個目錄,使得可以在虛擬電腦內取用。請緊記,分享目錄的支援視乎客戶端作業系統而定,可能需要安裝額外的客戶端驅動程式。有關更多詳細訊息,請見 UTM 支援頁面。";
+
+/* No comment provided by engineer. */
+"Options here only apply on next boot and are not saved." = "此處的選項僅於下次啟動時生效,且不會儲存。";
+
+/* No comment provided by engineer. */
+"Path" = "路徑";
+
+/* No comment provided by engineer. */
+"Port" = "埠";
+
+/* No comment provided by engineer. */
+"Power Off" = "關閉電源";
+
+/* No comment provided by engineer. */
+"Prompt" = "提示";
+
+/* No comment provided by engineer. */
+"Protocol" = "協定";
+
+/* No comment provided by engineer. */
+"QEMU Machine Properties" = "QEMU 電腦屬性";
+
+/* No comment provided by engineer. */
+"Quit" = "結束";
+
+/* No comment provided by engineer. */
+"RAM" = "記憶體";
+
+/* No comment provided by engineer. */
+"Ramdisk (optional)" = "Ramdisk (選填)";
+
+/* No comment provided by engineer. */
+"Random" = "隨機";
+
+/* No comment provided by engineer. */
+"Read Only?" = "唯讀?";
+
+/* No comment provided by engineer. */
+"Reclaim disk space by re-converting the disk image." = "透過重新轉換來回收磁碟空間。";
+
+/* No comment provided by engineer. */
+"Reclaim Space" = "釋放空間";
+
+/* No comment provided by engineer. */
+"Remove selected shortcut" = "移除已選取的捷徑";
+
+/* No comment provided by engineer. */
+"Renderer Backend" = "渲染器後端";
+
+/* No comment provided by engineer. */
+"Requires restarting UTM to take affect." = "需要重新開啟 UTM 以生效。";
+
+/* No comment provided by engineer. */
+"Requires SPICE guest agent tools to be installed." = "需要安裝 SPICE 客戶端代理程式工具。";
+
+/* No comment provided by engineer. */
+"Reset UEFI Variables" = "重設 UEFI 變數";
+
+/* No comment provided by engineer. */
+"Resize Console Command" = "調整主控台大小指令";
+
+/* No comment provided by engineer. */
+"Resize…" = "調整大小⋯";
+
+/* No comment provided by engineer. */
+"Resizing is experimental and could result in data loss. You are strongly encouraged to back-up this VM before proceeding. Would you like to resize to %lld GiB?" = "調整大小為實驗性功能,可能會導致資料遺失。強烈建議你在繼續之前備份此虛擬電腦。你要將大小調整為 %lld GB 嗎?";
+
+/* No comment provided by engineer. */
+"Resolution" = "解像度";
+
+/* No comment provided by engineer. */
+"Restart" = "重新啟動";
+
+/* No comment provided by engineer. */
+"Resume" = "繼續";
+
+/* No comment provided by engineer. */
+"Resume running VM." = "繼續正在執行的虛擬電腦。";
+
+/* No comment provided by engineer. */
+"Reveal where the VM is stored." = "顯示虛擬電腦的儲存位置。";
+
+/* No comment provided by engineer. */
+"RNG Device" = "RNG 裝置";
+
+/* No comment provided by engineer. */
+"Root Image" = "Root 映像檔";
+
+/* No comment provided by engineer. */
+"Run" = "執行";
+
+/* No comment provided by engineer. */
+"Run Recovery" = "執行 Recovery 模式";
+
+/* No comment provided by engineer. */
+"Run selected VM" = "執行已選取的虛擬電腦";
+
+/* No comment provided by engineer. */
+"Run the VM in the foreground." = "在螢幕前執行虛擬電腦。";
+
+/* No comment provided by engineer. */
+"Run the VM in the foreground, without saving data changes to disk." = "在螢幕前執行虛擬電腦,但無需將資料更改儲存到磁碟。";
+
+/* No comment provided by engineer. */
+"Run without saving changes" = "執行但不儲存更改";
+
+/* No comment provided by engineer. */
+"Section" = "區域";
+
+/* No comment provided by engineer. */
+"Secure Boot with TPM 2.0" = "使用 TPM 2.0 的保安啟動";
+
+/* No comment provided by engineer. */
+"Select an existing disk image." = "選取一個現存的磁碟映像。";
+
+/* No comment provided by engineer. */
+"Serial" = "序列";
+
+/* No comment provided by engineer. */
+"Server Address" = "伺服器位址";
+
+/* No comment provided by engineer. */
+"Settings" = "設定";
+
+/* No comment provided by engineer. */
+"Share" = "分享";
+
+/* No comment provided by engineer. */
+"Share…" = "分享⋯";
+
+/* No comment provided by engineer. */
+"Share a copy of this VM and all its data." = "分享此虛擬電腦與其所有資料的複製。";
+
+/* No comment provided by engineer. */
+"Share Directory" = "分享目錄";
+
+/* No comment provided by engineer. */
+"Share is read only" = "分享資料唯讀";
+
+/* No comment provided by engineer. */
+"Share selected VM" = "分享已選取的虛擬電腦";
+
+/* No comment provided by engineer. */
+"Shared Directory Path" = "分享目錄路徑";
+
+/* No comment provided by engineer. */
+"Shared Path" = "分享路徑";
+
+/* No comment provided by engineer. */
+"Should be off for older operating systems such as Windows 7 or lower." = "對於較舊的作業系統應關閉,例如 Windows 7 或者更低版本。";
+
+/* No comment provided by engineer. */
+"Should be on always unless the guest cannot boot because of this." = "除非客戶端因此而無法啟動,否則應始終開啟。";
+
+/* No comment provided by engineer. */
+"Show all devices…" = "顯示所有裝置⋯";
+
+/* No comment provided by engineer. */
+"Show in Finder" = "在 Finder 中顯示";
+
+/* No comment provided by engineer. */
+"Show the main window." = "顯示主視窗。";
+
+/* No comment provided by engineer. */
+"Show UTM" = "顯示 UTM";
+
+/* No comment provided by engineer. */
+"Show UTM preferences" = "顯示 UTM 偏好設定";
+
+/* No comment provided by engineer. */
+"Skip Boot Image" = "略過啟動映像檔";
+
+/* No comment provided by engineer. */
+"Skip ISO boot" = "略過 ISO 啟動";
+
+/* No comment provided by engineer. */
+"Some older systems do not support UEFI boot, such as Windows 7 and below." = "一些舊版系統不支援 UEFI 啟動,比如 Windows 7 及更舊版本。";
+
+/* No comment provided by engineer. */
+"Sound" = "聲音";
+
+/* No comment provided by engineer. */
+"Sound Backend" = "聲音後端";
+
+/* No comment provided by engineer. */
+"Start" = "開始";
+
+/* No comment provided by engineer. */
+"Status" = "狀態";
+
+/* No comment provided by engineer. */
+"Stop selected VM" = "停止已選取的虛擬電腦";
+
+/* No comment provided by engineer. */
+"Stop the running VM." = "停止正在執行的虛擬電腦。";
+
+/* No comment provided by engineer. */
+"Storage" = "儲存空間";
+
+/* No comment provided by engineer. */
+"stty cols $COLS rows $ROWS\n" = "stty cols $COLS rows $ROWS\n";
+
+/* No comment provided by engineer. */
+"Suspend" = "暫停";
+
+/* No comment provided by engineer. */
+"Target" = "目標";
+
+/* No comment provided by engineer. */
+"Terminate UTM and stop all running VMs." = "終止 UTM 並停止所有正在執行的虛擬電腦。";
+
+/* No comment provided by engineer. */
+"Text" = "文字";
+
+/* No comment provided by engineer. */
+"Text Color" = "文字顏色";
+
+/* No comment provided by engineer. */
+"The amount of storage to allocate for this image. Ignored if importing an image. If this is a raw image, then an empty file of this size will be stored with the VM. Otherwise, the disk image will dynamically expand up to this size." = "為此映像檔分配的儲存量。於輸入映像檔時會忽略此參數。如果此為一個 Raw 映像檔,則此大小的空檔案將會與虛擬電腦一齊儲存。否則,磁碟映像檔將會動態擴充至此大小。";
+
+/* No comment provided by engineer. */
+"Theme" = "主題";
+
+/* No comment provided by engineer. */
+"There are known issues in some newer Linux drivers including black screen, broken compositing, and apps failing to render." = "一些較新的 Linux 驅動程式存在已知問題,當中包括螢幕變黑、顯示合成損毀,以及應用程式無法渲染。";
+
+/* No comment provided by engineer. */
+"These are advanced settings affecting QEMU which should be kept default unless you are running into issues." = "這些為影響 QEMU 的進階設定,除非遇到問題,否則你應當保持預設值。";
+
+/* No comment provided by engineer. */
+"This is appended to the -machine argument." = "這會加至 -machine 參數的尾端。";
+
+/* No comment provided by engineer. */
+"This virtual machine cannot be found at: %@" = "虛擬電腦無法於此處找到:%@";
+
+/* No comment provided by engineer. */
+"This virtual machine must be re-added to UTM by opening it with Finder. You can find it at the path: %@" = "必須使用 Finder 開啟此虛擬電腦,將其重新加至 UTM 中。你可以於此路徑中找到它:%@";
+
+/* No comment provided by engineer. */
+"TPM 2.0 Device" = "TPM 2.0 裝置";
+
+/* No comment provided by engineer. */
+"TPM can be used to protect secrets in the guest operating system. Note that the host will always be able to read these secrets and therefore no expectation of physical security is provided." = "TPM 可以用來保護客戶端作業系統中的私密資料。請緊記,主機將始終可以讀取這些私密資料,因此無法提供預期的物理保安性。";
+
+/* UTMAppleConfigurationDevices */
+"Trackpad" = "觸控板";
+
+/* No comment provided by engineer. */
+"Tweaks" = "調整";
+
+/* No comment provided by engineer. */
+"Ubuntu Install Guide" = "Ubuntu 安裝指引";
+
+/* No comment provided by engineer. */
+"UEFI Boot" = "UEFI 啟動";
+
+/* No comment provided by engineer. */
+"Upscaling" = "粗化解像度";
+
+/* No comment provided by engineer. */
+"USB Support" = "USB 支援";
+
+/* No comment provided by engineer. */
+"Use Apple Virtualization" = "使用 Apple 虛擬化";
+
+/* No comment provided by engineer. */
+"Use Hypervisor" = "使用 Hypervisor";
+
+/* No comment provided by engineer. */
+"Use local time for base clock" = "使用本地時間作為基本時鐘";
+
+/* No comment provided by engineer. */
+"Use Rosetta" = "使用 Rosetta";
+
+/* No comment provided by engineer. */
+"Use Trackpad" = "使用觸控板";
+
+/* No comment provided by engineer. */
+"Use TSO" = "使用 TSO";
+
+/* No comment provided by engineer. */
+"Use Virtualization" = "使用虛擬化";
+
+/* No comment provided by engineer. */
+"VGA Device RAM (MB)" = "VGA 裝置記憶體 (MB)";
+
+/* No comment provided by engineer. */
+"Virtualization" = "虛擬化";
+
+/* No comment provided by engineer. */
+"Virtualization Engine" = "虛擬化引擎";
+
+/* No comment provided by engineer. */
+"Wait for Connection" = "等待連線";
+
+/* No comment provided by engineer. */
+"WebDAV requires installing SPICE daemon. VirtFS requires installing device drivers." = "WebDAV 需要安裝 SPICE 守護程式。VirtFS 需要安裝裝置驅動程式。";
+
+/* No comment provided by engineer. */
+"Width" = "寬度";
+
+/* No comment provided by engineer. */
+"Windows Install Guide" = "Windows 安裝指引";
+
+/* No comment provided by engineer. */
+"You can use this if your boot options are corrupted or if you wish to re-enroll in the default keys for secure boot." = "如果你的引導選項已經損毀,或者你希望重新登入保安啟動的預設鑰匙,可以使用此選項。";
+
+/* No comment provided by engineer. */
+"Zoom" = "縮放";
diff --git a/Platform/zh-Hans.lproj/Localizable.strings b/Platform/zh-Hans.lproj/Localizable.strings
index 85c2e7145..875129212 100644
--- a/Platform/zh-Hans.lproj/Localizable.strings
+++ b/Platform/zh-Hans.lproj/Localizable.strings
@@ -66,13 +66,13 @@
"Advanced" = "高级";
/* VMConfigSystemView */
-"Allocating too much memory will crash the VM." = "分配过多的内存会导致虚拟机崩溃。";
+"Allocating too much memory will crash the VM." = "分配过多内存会使虚拟机崩溃。";
/* UTMData */
-"AltJIT error: %@" = "AltJIT 错误: %@";
+"AltJIT error: %@" = "AltJIT 错误:%@";
/* UTMData */
-"An existing virtual machine already exists with this name." = "已经存在一个具有此名称的虚拟机。";
+"An existing virtual machine already exists with this name." = "已存在一个有此名称的虚拟机。";
/* UTMConfiguration */
"An internal error has occurred." = "发生了内部错误。";
@@ -164,7 +164,7 @@
"Cancel" = "取消";
/* UTMAppleVirtualMachine */
-"Cannot access resource: %@" = "无法访问资源: %@";
+"Cannot access resource: %@" = "无法访问资源:%@";
/* UTMSWTPM */
"Cannot access TPM data." = "无法访问 TPM 数据。";
@@ -173,10 +173,10 @@
"Cannot create virtual terminal." = "无法创建虚拟终端。";
/* UTMData */
-"Cannot find AltServer for JIT enable. You cannot run VMs until JIT is enabled." = "找不到用于 JIT 启用的 AltServer。在启用 JIT 之前,你无法运行虚拟机。";
+"Cannot find AltServer for JIT enable. You cannot run VMs until JIT is enabled." = "找不到用于 JIT 启用的 AltServer。在启用 JIT 之前,你将无法运行虚拟机。";
/* UTMData */
-"Cannot import this VM. Either the configuration is invalid, created in a newer version of UTM, or on a platform that is incompatible with this version of UTM." = "无法导入此虚拟机。此虚拟机配置无效,或是在较新版本的 UTM 中创建,或是在与此版本的 UTM 不兼容的平台上创建。";
+"Cannot import this VM. Either the configuration is invalid, created in a newer version of UTM, or on a platform that is incompatible with this version of UTM." = "无法导入此虚拟机。此虚拟机可能配置无效,或者可能是在较新版本的 UTM 中创建的,也可能是在与此版本的 UTM 不兼容的平台上创建的。";
/* No comment provided by engineer. */
"Caps Lock (⇪) is treated as a key" = "将 Caps Lock (⇪) 视为按键";
@@ -188,7 +188,7 @@
"Capture input automatically when entering full screen" = "进入全屏时自动捕获输入";
/* VMDisplayQemuMetalWindowController */
-"Captured mouse" = "鼠标已捕获";
+"Captured mouse" = "已捕获鼠标";
/* Configuration boot device */
"CD/DVD" = "CD/DVD";
@@ -215,7 +215,8 @@
/* No comment provided by engineer. */
"Confirm Delete" = "确认删除";
-/* VMDisplayWindowController */
+/* AppDelegate
+ VMDisplayWindowController */
"Confirmation" = "确认";
/* No comment provided by engineer. */
@@ -281,13 +282,13 @@
"Display" = "显示";
/* VMDisplayQemuDisplayController */
-"Display %lld: %@" = "显示 %1$lld: %2$@";
+"Display %lld: %@" = "显示 %1$lld:%2$@";
/* VMDisplayQemuDisplayController */
"Disposable Mode" = "一次性模式";
/* No comment provided by engineer. */
-"Do not save VM screenshot to disk" = "不要将虚拟机屏幕截图保存到磁盘";
+"Do not save VM screenshot to disk" = "不将虚拟机的屏幕截图保存到磁盘";
/* No comment provided by engineer. */
"Do not show confirmation when closing a running VM" = "关闭正在运行的虚拟机时不显示确认";
@@ -308,16 +309,16 @@
"Do you want to force stop this VM and lose all unsaved data?" = "要强制停止此虚拟机并丢失所有未保存的数据吗?";
/* No comment provided by engineer. */
-"Do you want to move this VM to another location? This will copy the data to the new location, delete the data from the original location, and then create a shortcut." = "要将此虚拟机移动到别处吗?这将把数据复制到新位置,从原始位置删除数据,然后创建一个快捷方式。";
+"Do you want to move this VM to another location? This will copy the data to the new location, delete the data from the original location, and then create a shortcut." = "要将此虚拟机移动到别处吗?这将会复制数据到新位置,删除原始位置的数据,然后创建一个快捷方式。";
/* No comment provided by engineer. */
-"Do you want to remove this shortcut? The data will not be deleted." = "要删除这个快捷方式吗?数据不会被删除。";
+"Do you want to remove this shortcut? The data will not be deleted." = "要删除此快捷方式吗?数据不会被删除。";
/* No comment provided by engineer. */
-"Download prebuilt from UTM Gallery…" = "从 UTM 库中下载预构建的虚拟机…";
+"Download prebuilt from UTM Gallery…" = "从 UTM 库中下载预构建虚拟机…";
/* No comment provided by engineer. */
-"Drag and drop IPSW file here" = "在此处拖放 IPSW 文件";
+"Drag and drop IPSW file here" = "拖放 IPSW 文件到此处";
/* UTMScriptingConfigImpl */
"Drive description is invalid." = "驱动器描述无效。";
@@ -362,7 +363,7 @@
"Failed to access shared directory." = "无法访问共享目录。";
/* ContentView */
-"Failed to attach to JitStreamer:\n%@" = "未能附加到 JitStreamer: %@";
+"Failed to attach to JitStreamer:\n%@" = "未能附加到 JitStreamer:%@";
/* UTMData */
"Failed to attach to JitStreamer." = "未能附加到 JitStreamer。";
@@ -371,7 +372,7 @@
"Failed to change current directory." = "更改当前目录失败。";
/* UTMData */
-"Failed to clone VM." = "克隆虚拟机失败。";
+"Failed to clone VM." = "复制虚拟机失败。";
/* UTMData */
"Failed to decode JitStreamer response." = "未能解码 JitStreamer 响应。";
@@ -383,19 +384,20 @@
"Failed to migrate configuration from a previous UTM version." = "无法从以前的 UTM 版本迁移配置。";
/* UTMData */
-"Failed to parse download URL." = "未能解析下载 URL。";
+"Failed to parse download URL." = "无法解析下载 URL。";
/* UTMData */
"Failed to parse imported VM." = "无法解析导入的虚拟机。";
/* UTMDownloadVMTask */
-"Failed to parse the downloaded VM." = "解析下载的虚拟机失败。";
+"Failed to parse the downloaded VM." = "无法解析下载的虚拟机。";
-/* VMDisplayWindowController */
+/* AppDelegate
+ VMDisplayWindowController */
"Failed to save suspend state" = "无法保存挂起状态。";
/* UTMQemuVirtualMachine */
-"Failed to save VM snapshot. Usually this means at least one device does not support snapshots. %@" = "保存虚拟机快照失败。通常这意味着至少有一台设备不支持快照。%@";
+"Failed to save VM snapshot. Usually this means at least one device does not support snapshots. %@" = "保存虚拟机快照失败。通常这意味着至少有一个设备不支持快照。%@";
/* UTMSpiceIO */
"Failed to start SPICE client." = "无法启动 SPICE 客户端。";
@@ -414,7 +416,7 @@
"Force kill" = "强制终止";
/* VMDisplayWindowController */
-"Force kill the VM process with high risk of data corruption." = "强制杀死具有高数据损坏风险的虚拟机进程。";
+"Force kill the VM process with high risk of data corruption." = "强制杀死虚拟机进程 (会有高风险数据损坏)。";
/* No comment provided by engineer. */
"Force Multicore" = "强制多核";
@@ -431,6 +433,12 @@
/* No comment provided by engineer. */
"Generic" = "通用";
+/* UTMAppleConfigurationDevices */
+"Generic Mouse" = "通用鼠标";
+
+/* UTMAppleConfigurationDevices */
+"Generic USB" = "通用 USB";
+
/* No comment provided by engineer. */
"Gesture and Cursor Settings" = "手势和光标设置";
@@ -486,7 +494,7 @@
"Install Windows Guest Tools…" = "安装 Windows 客户机工具…";
/* VMDisplayAppleWindowController */
-"Installation: %@" = "安装: %@";
+"Installation: %@" = "安装:%@";
/* UTMProcess */
"Internal error has occurred." = "发生内部错误。";
@@ -498,7 +506,7 @@
"Internal error." = "内部错误。";
/* UTMData */
-"Invalid JitStreamer attach URL:\n%@" = "无效的 JitStreamer 附加 URL: %@";
+"Invalid JitStreamer attach URL:\n%@" = "无效的 JitStreamer 附加 URL:%@";
/* VMConfigAppleNetworkingView */
"Invalid MAC address." = "MAC 地址无效。";
@@ -519,7 +527,7 @@
"Italic, Bold" = "斜体,粗体";
/* No comment provided by engineer. */
-"Keep UTM running after last window is closed and all VMs are shut down" = "最后一个窗口关闭且所有虚拟机关机后保持 UTM 运行";
+"Keep UTM running after last window is closed and all VMs are shut down" = "在关闭最后一个窗口和关闭所有虚拟机后继续运行 UTM";
/* No comment provided by engineer. */
"License" = "许可";
@@ -557,6 +565,12 @@
/* No comment provided by engineer. */
"Logging" = "日志";
+/* UTMAppleConfigurationDevices */
+"Mac Keyboard (macOS 14+)" = "Mac 键盘 (macOS 14+)";
+
+/* UTMAppleConfigurationDevices */
+"Mac Trackpad (macOS 13+)" = "Mac 触控板 (macOS 14+)";
+
/* UTMAppleConfigurationBoot */
"macOS" = "macOS";
@@ -585,10 +599,7 @@
"Metal is not supported on this device. Cannot render display." = "此设备不支持 Metal。无法渲染显示。";
/* No comment provided by engineer. */
-"Minimum size: %@" = "最小文件大小: %@";
-
-/* UTMAppleConfigurationDevices */
-"Mouse" = "鼠标";
+"Minimum size: %@" = "最小文件大小:%@";
/* No comment provided by engineer. */
"Mouse/Keyboard" = "鼠标/键盘";
@@ -618,19 +629,19 @@
"No" = "否";
/* UTMScriptingAppDelegate */
-"No architecture specified in the configuration." = "未在配置中指定架构。";
+"No architecture specified in the configuration." = "配置中未指定架构。";
/* VMDisplayWindowController */
-"No drives connected." = "未连接驱动器。";
+"No drives connected." = "没有连接的驱动器。";
/* UTMDownloadSupportToolsTaskError */
"No empty removable drive found. Make sure you have at least one removable drive that is not in use." = "未找到空的可移动驱动器。确保你至少有一个未使用的可移动驱动器。";
/* UTMScriptingAppDelegate */
-"No name specified in the configuration." = "未在配置中指定名称。";
+"No name specified in the configuration." = "配置中未指定名称。";
/* No comment provided by engineer. */
-"No output device is selected for this window." = "没有为此窗口选择输出设备。";
+"No output device is selected for this window." = "此窗口未选择输出设备。";
/* No comment provided by engineer. */
"No release notes found for version %@." = "未找到 %@ 版本的发布说明。";
@@ -682,7 +693,7 @@
"Operation not supported by the backend." = "操作不受后端支持。";
/* No comment provided by engineer. */
-"Option (⌥) is Meta key" = "将 Option (⌥) 键作为 Meta 键";
+"Option (⌥) is Meta key" = "Option (⌥) 键作为 Meta 键";
/* No comment provided by engineer. */
"Other" = "其他";
@@ -700,7 +711,7 @@
"PC System Flash" = "PC 系统闪存";
/* No comment provided by engineer. */
-"Pending" = "正在队列";
+"Pending" = "等待中";
/* VMDisplayWindowController */
"Play" = "启动";
@@ -805,7 +816,7 @@
"Resize display to window size automatically" = "自动将显示调整为窗口大小";
/* No comment provided by engineer. */
-"Resizing is experimental and could result in data loss. You are strongly encouraged to back-up this VM before proceeding. Would you like to resize to %@ GiB?" = "调整驱动器大小是实验性功能,可能导致数据丢失。在继续操作之前,强烈建议你备份此虚拟机。要将大小调整为 %@ GB 吗?";
+"Resizing is experimental and could result in data loss. You are strongly encouraged to back-up this VM before proceeding. Would you like to resize to %@ GiB?" = "调整驱动器大小是实验性功能,可能会导致数据丢失。在继续操作之前,强烈建议你备份此虚拟机。要将大小调整为 %@ GB 吗?";
/* VMData */
"Restoring" = "正在恢复";
@@ -848,16 +859,16 @@
"Select Shared Folder" = "选择共享的文件夹";
/* SavePanel */
-"Select where to export QEMU command:" = "选择导出 QEMU 命令的位置:";
+"Select where to export QEMU command:" = "选择导出 QEMU 命令的位置:";
/* SavePanel */
-"Select where to save debug log:" = "选择保存调试日志的位置:";
+"Select where to save debug log:" = "选择保存调试日志的位置:";
/* SavePanel */
-"Select where to save UTM Virtual Machine:" = "选择保存 UTM 虚拟机的位置:";
+"Select where to save UTM Virtual Machine:" = "选择保存 UTM 虚拟机的位置:";
/* No comment provided by engineer. */
-"Selected:" = "已选择:";
+"Selected:" = "已选择:";
/* VMDisplayWindowController */
"Sends power down request to the guest. This simulates pressing the power button on a PC." = "向客户机发送关闭电源请求。此操作模拟了按住 PC 上的电源按钮。";
@@ -939,7 +950,7 @@
"Support" = "支持";
/* UTMQemuVirtualMachine */
-"Suspend is not supported for virtualization." = "虚拟化不支持挂起。";
+"Suspend is not supported for virtualization." = "挂起功能不支持虚拟化。";
/* UTMQemuVirtualMachine */
"Suspend is not supported when an emulated NVMe device is active." = "当模拟的 NVMe 设备处于活动状态时不支持挂起。";
@@ -993,7 +1004,7 @@
"The drive '%@' already exists and cannot be created." = "驱动器 '%@' 已存在,无法创建。";
/* UTMDownloadSupportToolsTaskError */
-"The guest support tools have already been mounted." = "客户机支持工具已在挂载。";
+"The guest support tools have already been mounted." = "客户机支持工具已挂载。";
/* UTMAppleConfiguration */
"The host operating system needs to be updated to support one or more features requested by the guest." = "需要更新主机的操作系统,以支持客户机请求的一个或多个功能。";
@@ -1011,7 +1022,7 @@
"The selected architecture is unsupported in this version of UTM." = "此版本的 UTM 不支持所选架构。";
/* VMWizardState */
-"The selected boot image contains the word '%@' but the guest architecture is '%@'. Please ensure you have selected an image that is compatible with '%@'." = "所选的引导映像名称包含 '%@',但客户机的架构为 %@。请确保你选择了与 '%@' 兼容的映像。";
+"The selected boot image contains the word '%@' but the guest architecture is '%@'. Please ensure you have selected an image that is compatible with '%@'." = "所选的引导映像名称包含 '%@',但客户机的架构为 '%@'。请确保你选择了与 '%@' 兼容的映像。";
/* No comment provided by engineer. */
"The target does not support hardware emulated serial connections." = "此目标系统不支持硬件模拟串行连接。";
@@ -1044,10 +1055,10 @@
"This change will reset all settings" = "此更改将重置所有设置。";
/* UTMConfiguration */
-"This configuration is saved with a newer version of UTM and is not compatible with this version." = "此配置是使用较新版本的 UTM 保存的,并且与此版本不兼容。";
+"This configuration is saved with a newer version of UTM and is not compatible with this version." = "此配置是用较新版本的 UTM 保存的,并且与此版本不兼容。";
/* UTMConfiguration */
-"This configuration is too old and is not supported." = "此配置因太旧而无法支持。";
+"This configuration is too old and is not supported." = "此配置过旧,无法支持。";
/* UTMScriptingConfigImpl */
"This device is not supported by the target." = "目标不支持此设备。";
@@ -1056,7 +1067,7 @@
"This directory is already being shared." = "此目录已被共享。";
/* UTMAppleConfiguration */
-"This is not a valid Apple Virtualization configuration." = "这不是有效的 Apple 虚拟化配置。";
+"This is not a valid Apple Virtualization configuration." = "并非有效的 Apple 虚拟化配置。";
/* VMDisplayWindowController */
"This may corrupt the VM and any unsaved changes will be lost. To quit safely, shut down from the guest." = "这可能会损坏虚拟机,任何未保存的更改都将丢失。为了安全退出,请从客户机操作系统关闭。";
@@ -1091,9 +1102,6 @@
/* VMDisplayQemuMetalWindowController */
"To release the mouse cursor, press %@ at the same time." = "要释放鼠标光标,请同时按 %@。";
-/* UTMAppleConfigurationDevices */
-"Trackpad" = "触控板";
-
/* No comment provided by engineer. */
"u{2022} " = "u{2022}";
@@ -1217,3 +1225,794 @@
/* ContentView */
"Your version of iOS does not support running VMs while unmodified. You must either run UTM while jailbroken or with a remote debugger attached. See https://getutm.app/install/ for more details." = "你的 iOS 版本不支持在未经修改的情况下运行虚拟机,必须在越狱时运行 UTM,或者连接远程调试器。有关更多详细信息,请参阅 https://getutm.app/install/。";
+// Additonal Strings (Unable to be extracted by Xcode)
+
+/* No comment provided by engineer. */
+"(Delete)" = "(删除)";
+
+/* No comment provided by engineer. */
+"Add" = "添加";
+
+/* No comment provided by engineer. */
+"Add a new device." = "添加一个新设备。";
+
+/* No comment provided by engineer. */
+"Add a new drive." = "添加一个新驱动器。";
+
+/* No comment provided by engineer. */
+"Add read only" = "添加只读";
+
+/* No comment provided by engineer. */
+"Advanced. If checked, a raw disk image is used. Raw disk image does not support snapshots and will not dynamically expand in size." = "高级选项。若选中,将使用 Raw 磁盘映像。Raw 磁盘映像不支持快照,也不会动态地扩充大小。";
+
+/* No comment provided by engineer. */
+"Allow Remote Connection" = "允许远程连接";
+
+/* No comment provided by engineer. */
+"Allows passing through additional input from trackpads. Only supported on macOS 13+ guests." = "允许通过触控板额外输入。仅支持 macOS 13 及以上的客户机。";
+
+/* No comment provided by engineer. */
+"Apple Virtualization is experimental and only for advanced use cases. Leave unchecked to use QEMU, which is recommended." = "Apple 虚拟化属于实验性功能,仅适用于高级用例。推荐不选中此复选框,以使用 QEMU。";
+
+/* No comment provided by engineer. */
+"Application" = "应用程序";
+
+/* No comment provided by engineer. */
+"Architecture" = "架构";
+
+/* No comment provided by engineer. */
+"Arguments" = "参数";
+
+/* No comment provided by engineer. */
+"Auto Resolution" = "自动调整分辨率";
+
+/* No comment provided by engineer. */
+"Automatic" = "自动";
+
+/* No comment provided by engineer. */
+"Background Color" = "背景颜色";
+
+/* No comment provided by engineer. */
+"Balloon Device" = "Balloon 设备";
+
+/* No comment provided by engineer. */
+"Blinking cursor?" = "闪烁光标?";
+
+/* No comment provided by engineer. */
+"Boot arguments" = "启动参数";
+
+/* No comment provided by engineer. */
+"Boot Arguments" = "启动参数";
+
+/* No comment provided by engineer. */
+"Boot from kernel image" = "从内核映像启动";
+
+/* No comment provided by engineer. */
+"Boot Image" = "启动映像";
+
+/* No comment provided by engineer. */
+"Boot Image Type" = "启动映像类型";
+
+/* No comment provided by engineer. */
+"Boot into recovery mode." = "启动到还原模式。";
+
+/* No comment provided by engineer. */
+"Bootloader" = "Bootloader";
+
+/* No comment provided by engineer. */
+"Bridged Interface" = "桥接接口";
+
+/* No comment provided by engineer. */
+"Bridged Settings" = "桥接设置";
+
+/* No comment provided by engineer. */
+"By default, the best backend for the target will be used. If the selected backend is not available for any reason, an alternative will automatically be selected." = "默认情況下,将使用目标虚拟机的最佳后端。若所选的后端因任何原因而不可用,将自动选择替代方案。";
+
+/* No comment provided by engineer. */
+"By default, the best renderer for this device will be used. You can override this with to always use a specific renderer. This only applies to QEMU VMs with GPU accelerated graphics." = "默认情況下,将使用最适合此设备的渲染器。你可以覆盖此选项,以始终使用特定的渲染器。此选项仅适用于具有 GPU 加速图形的 QEMU 虚拟机。";
+
+/* No comment provided by engineer. */
+"Calculating current size..." = "计算当前大小…";
+
+/* No comment provided by engineer. */
+"Cancel Download" = "取消下载";
+
+/* No comment provided by engineer. */
+"Clipboard Sharing" = "剪贴板共享";
+
+/* No comment provided by engineer. */
+"Clone" = "复制";
+
+/* No comment provided by engineer. */
+"Clone selected VM" = "复制已选中的虚拟机";
+
+/* No comment provided by engineer. */
+"Clone…" = "复制…";
+
+/* No comment provided by engineer. */
+"Close" = "关闭";
+
+/* No comment provided by engineer. */
+"Closing a VM without properly shutting it down could result in data loss." = "在不正确关闭电源的情况下关闭虚拟机可能会导致数据丢失。";
+
+/* No comment provided by engineer. */
+"Compress" = "压缩";
+
+/* No comment provided by engineer. */
+"Compress by re-converting the disk image and compressing the data." = "通过重新转换磁盘映像和压缩数据映像来实现压缩。";
+
+/* No comment provided by engineer. */
+"Create a new VM" = "创建一个新虚拟机";
+
+/* No comment provided by engineer. */
+"Create a new VM with the same configuration as this one but without any data." = "创建一个和此配置相同,但不带有任何数据的新虚拟机。";
+
+/* No comment provided by engineer. */
+"Create an empty drive." = "创建一个空驱动器。";
+
+/* No comment provided by engineer. */
+"Debian Install Guide" = "Debian 安装指南";
+
+/* No comment provided by engineer. */
+"Default is 1/4 of the RAM size (above). The JIT cache size is additive to the RAM size in the total memory usage!" = "默认为内存大小的 1/4 (如上)。在总内存使用量中,JIT 缓存与内存的大小都会包括在内!";
+
+/* No comment provided by engineer. */
+"Delete this drive." = "删除此驱动器。";
+
+/* No comment provided by engineer. */
+"Delete selected VM" = "刪除已选中的虚拟机";
+
+/* No comment provided by engineer. */
+"Delete this shortcut. The underlying data will not be deleted." = "删除此快捷方式。快捷方式背后的数据不会被删除。";
+
+/* No comment provided by engineer. */
+"Delete this VM and all its data." = "刪除此虚拟机及其所有数据。";
+
+/* No comment provided by engineer. */
+"Delete Drive" = "刪除驱动器";
+
+/* No comment provided by engineer. */
+"Description" = "注释";
+
+/* No comment provided by engineer. */
+"Devices" = "设备";
+
+/* No comment provided by engineer. */
+"Directory" = "目录";
+
+/* No comment provided by engineer. */
+"Directory Share Mode" = "目录共享模式";
+
+/* No comment provided by engineer. */
+"Disk" = "磁盘";
+
+/* No comment provided by engineer. */
+"DHCP Domain Name" = "DHCP 域名";
+
+/* No comment provided by engineer. */
+"DHCP End" = "DHCP 结束地址";
+
+/* No comment provided by engineer. */
+"DNS Search Domains" = "DNS 搜索域";
+
+/* No comment provided by engineer. */
+"DNS Server" = "DNS 服务器";
+
+/* No comment provided by engineer. */
+"DNS Server (IPv6)" = "DNS 服务器 (IPv6)";
+
+/* No comment provided by engineer. */
+"DHCP Start" = "DHCP 起始地址";
+
+/* No comment provided by engineer. */
+"Done" = "完成";
+
+/* No comment provided by engineer. */
+"Duplicate this VM along with all its data." = "复制此虚拟机及其所有数据。";
+
+/* No comment provided by engineer. */
+"Download and mount the guest support package for Windows. This is required for some features including dynamic resolution and clipboard sharing." = "下载并装载 Windows 的客户机支持包。此支持包对于一些功能而言为必需,例如动态分辨率与剪贴板共享。";
+
+/* No comment provided by engineer. */
+"Download and mount the guest tools for Windows." = "下载并装载 Windows 客户机工具。";
+
+/* No comment provided by engineer. */
+"Download Windows 11 for ARM64 Preview VHDX" = "下载 Windows 11 ARM64 Preview VHDX 映像";
+
+/* No comment provided by engineer. */
+"Downscaling" = "细化";
+
+/* No comment provided by engineer. */
+"Edit" = "编辑";
+
+/* No comment provided by engineer. */
+"Edit selected VM" = "编辑已选中的虚拟机";
+
+/* No comment provided by engineer. */
+"Edit…" = "编辑…";
+
+/* No comment provided by engineer. */
+"Emulated Audio Card" = "模拟声卡";
+
+/* No comment provided by engineer. */
+"Emulated Display Card" = "模拟显卡";
+
+/* No comment provided by engineer. */
+"Emulated Network Card" = "模拟网卡";
+
+/* No comment provided by engineer. */
+"Emulated Serial Device" = "模拟序列设备";
+
+/* No comment provided by engineer. */
+"Enable Balloon Device" = "启用 Balloon 设备";
+
+/* No comment provided by engineer. */
+"Enable Entropy Device" = "启用 Entropy 设备";
+
+/* No comment provided by engineer. */
+"Enable hardware OpenGL acceleration" = "启用 OpenGL 硬件加速";
+
+/* No comment provided by engineer. */
+"Enable Keyboard" = "启用键盘";
+
+/* No comment provided by engineer. */
+"Enable Pointer" = "启用光标";
+
+/* No comment provided by engineer. */
+"Enable Rosetta (x86_64 Emulation)" = "启用 Rosetta (x86_64 模拟)";
+
+/* No comment provided by engineer. */
+"Enable Rosetta on Linux (x86_64 Emulation)" = "在 Linux 中启用 Rosetta (x86_64 模拟)";
+
+/* No comment provided by engineer. */
+"Enable Sound" = "启用声音";
+
+/* No comment provided by engineer. */
+"Engine" = "引擎";
+
+/* No comment provided by engineer. */
+"Export all arguments as a text file. This is only for debugging purposes as UTM's built-in QEMU differs from upstream QEMU in supported arguments." = "将所有参数导出为文本文件。此功能仅用于调试目的,因为 UTM 的内置 QEMU 与上游 QEMU 在支持的参数上有所不同。";
+
+/* No comment provided by engineer. */
+"Export Debug Log" = "导出调试日志";
+
+/* No comment provided by engineer. */
+"External Drive" = "外部磁盘";
+
+/* No comment provided by engineer. */
+"Fetch latest Windows installer…" = "获取最新的 Windows 安装程序…";
+
+/* No comment provided by engineer. */
+"Font" = "字体";
+
+/* No comment provided by engineer. */
+"Force Disable CPU Flags" = "强制禁用 CPU 标志";
+
+/* No comment provided by engineer. */
+"Force Enable CPU Flags" = "强制启用 CPU 标志";
+
+/* No comment provided by engineer. */
+"Force multicore may improve speed of emulation but also might result in unstable and incorrect emulation." = "强制多核可以提高模拟速度,但也可能导致模拟不稳定和不正确。";
+
+/* No comment provided by engineer. */
+"Force PS/2 controller" = "强制使用 PS/2 控制器";
+
+/* No comment provided by engineer. */
+"FPS Limit" = "FPS 限制";
+
+/* No comment provided by engineer. */
+"Go Back" = "返回";
+
+/* No comment provided by engineer. */
+"GPU Acceleration Supported" = "支持 GPU 加速";
+
+/* No comment provided by engineer. */
+"Guest Address" = "客户机地址";
+
+/* No comment provided by engineer. */
+"Guest Network" = "客户机网络";
+
+/* No comment provided by engineer. */
+"Guest Network (IPv6)" = "客户机网络 (IPv6)";
+
+/* No comment provided by engineer. */
+"Guest Port" = "客户机端口";
+
+/* No comment provided by engineer. */
+"Hardware interface on the guest used to mount this image. Different operating systems support different interfaces. The default will be the most common interface." = "用于挂载此映像的客户机硬件接口。不同的操作系统支持不同的接口。默认值为最常用的接口。";
+
+/* No comment provided by engineer. */
+"Hardware OpenGL Acceleration" = "硬件 OpenGL 加速";
+
+/* No comment provided by engineer. */
+"Height" = "高度";
+
+/* No comment provided by engineer. */
+"Hide" = "隐藏";
+
+/* No comment provided by engineer. */
+"Hide dock icon on next launch" = "下次启动时隐藏程序坞图标";
+
+/* No comment provided by engineer. */
+"Host Address" = "主机地址";
+
+/* No comment provided by engineer. */
+"Host Address (IPv6)" = "主机地址 (IPv6)";
+
+/* No comment provided by engineer. */
+"Host Port" = "主机端口";
+
+/* No comment provided by engineer. */
+"If checked, no drive image will be stored with the VM. Instead you can mount/unmount image while the VM is running." = "若选中,虚拟机将不会存储驱动器映像。相反,你可以在虚拟机运行时挂载/卸载映像。";
+
+/* No comment provided by engineer. */
+"If checked, the CPU flag will be enabled. Otherwise, the default value will be used." = "若选中,将启用 CPU 标志。否则将使用默认值。";
+
+/* No comment provided by engineer. */
+"If checked, the CPU flag will be disabled. Otherwise, the default value will be used." = "若选中,将禁用 CPU 标志。否则将使用默认值。";
+
+/* No comment provided by engineer. */
+"If checked, the drive image will be stored with the VM." = "若选中,驱动器映像将和虚拟机一起存储。";
+
+/* No comment provided by engineer. */
+"If checked, use local time for RTC which is required for Windows. Otherwise, use UTC clock." = "若选中,将使用 Windows 需要的 RTC 本地时间,否则使用 UTC 时钟。";
+
+/* No comment provided by engineer. */
+"If disabled, the default combination Control+Option (⌃+⌥) will be used." = "若禁用,将使用默认组合键 Control + Option (⌃ + ⌥)。";
+
+/* No comment provided by engineer. */
+"If enabled, a virtiofs share tagged 'rosetta' will be available on the Linux guest for installing Rosetta for emulating x86_64 on ARM64." = "若启用,标为“rosetta”的 virtiofs 共享将在 Linux 客户机上可用,用于安装 Rosetta,并在 arm64 上模拟 x86_64。";
+
+/* No comment provided by engineer. */
+"If enabled, any existing screenshot will be deleted the next time the VM is started." = "若启用,下次启动虚拟机时,任何现有的快照都将被删除。";
+
+/* No comment provided by engineer. */
+"If enabled, caps lock will be handled like other keys. If disabled, it is treated as a toggle that is synchronized with the host." = "若启用,Caps Lock 将和其他按键一样处理。若禁用,它将被视为与主机同步的切换键。";
+
+/* No comment provided by engineer. */
+"If enabled, input capture will toggle automatically when entering and exiting full screen mode." = "若启用,输入捕捉会在进入和退出全屏模式时自动切换。";
+
+/* No comment provided by engineer. */
+"If enabled, num lock will always be on to the guest. Note this may make your keyboard's num lock indicator out of sync." = "若启用,Num Lock 将始终对客户机开启。注意,这可能会使键盘的 Num Lock 指示灯不同步。";
+
+/* No comment provided by engineer. */
+"If enabled, Option will be mapped to the Meta key which can be useful for emacs. Otherwise, option will work as the system intended (such as for entering international text)." = "若启用,Option 键将映射到 Meta 键,这对 Emacs 很有用。否则,Option 键将按照系统默认方式工作(例如输入国际文本)。";
+
+/* No comment provided by engineer. */
+"If enabled, resizing of the VM window will not be allowed." = "若启用,将不允许调整虚拟机窗口的大小。";
+
+/* No comment provided by engineer. */
+"If enabled, scroll wheel input will be inverted." = "若启用,将反转滚轮输入。";
+
+/* No comment provided by engineer. */
+"If enabled, the default input devices will be emulated on the USB bus." = "若启用,默认输入设备将在 USB 总线上模拟。";
+
+/* No comment provided by engineer. */
+"If set, a frame limit can improve smoothness in rendering by preventing stutters when set to the lowest value your device can handle." = "若设置,则当设置为设备可以处理的最低值时,帧限制可以防止卡顿,从而提高渲染的流畅度。";
+
+/* No comment provided by engineer. */
+"If set, boot directly from a raw kernel image and initrd. Otherwise, boot from a supported ISO." = "如设置,直接由 Raw 内核映像和 initrd 启动。否則由受支持的 ISO 启动。";
+
+/* No comment provided by engineer. */
+"Image Type" = "映像类型";
+
+/* No comment provided by engineer. */
+"Import Drive" = "导入驱动器";
+
+/* No comment provided by engineer. */
+"Import VHDX Image" = "导入 VHDX 映像";
+
+/* No comment provided by engineer. */
+"Increase the size of the disk image." = "增加磁盘映像的大小。";
+
+/* No comment provided by engineer. */
+"Initial Ramdisk" = "初始 ramdisk";
+
+/* No comment provided by engineer. */
+"Input" = "输入";
+
+/* No comment provided by engineer. */
+"Install drivers and SPICE tools" = "安装驱动程序和 SPICE 工具";
+
+/* No comment provided by engineer. */
+"Install Windows 10 or higher" = "安装 Windows 10 或更高版本";
+
+/* No comment provided by engineer. */
+"Installation Instructions" = "安装指南";
+
+/* No comment provided by engineer. */
+"Instantiate PS/2 controller even when USB input is supported. Required for older Windows." = "即便支持 USB 输入,仍然实例化 PS/2 控制器。此功能对于旧版 Windows 为必需。";
+
+/* No comment provided by engineer. */
+"Interface" = "接口";
+
+/* No comment provided by engineer. */
+"IPSW Install Image" = "IPSW 安装映像";
+
+/* No comment provided by engineer. */
+"JIT Cache" = "JIT 缓存数据";
+
+/* No comment provided by engineer. */
+"Kernel" = "内核";
+
+/* No comment provided by engineer. */
+"Kernel Image" = "内核映像";
+
+/* No comment provided by engineer. */
+"Keyboard" = "键盘";
+
+/* No comment provided by engineer. */
+"MAC Address" = "MAC 地址";
+
+/* No comment provided by engineer. */
+"Machine" = "虚拟机";
+
+/* No comment provided by engineer. */
+"Maintenance" = "维护";
+
+/* No comment provided by engineer. */
+"Mode" = "模式";
+
+/* No comment provided by engineer. */
+"Modify settings for this VM." = "修改此虚拟机的设置。";
+
+/* UTMAppleConfigurationDevices */
+"Mouse" = "鼠标";
+
+/* No comment provided by engineer. */
+"Move" = "移动";
+
+/* No comment provided by engineer. */
+"Move…" = "移动…";
+
+/* No comment provided by engineer. */
+"Move selected VM" = "移动已选中的虚拟机";
+
+/* No comment provided by engineer. */
+"Move this VM from internal storage to elsewhere." = "将此虚拟机从内部存储空间移动到其他地方。";
+
+/* No comment provided by engineer. */
+"Network" = "网络";
+
+/* No comment provided by engineer. */
+"Network Mode" = "网络模式";
+
+/* No comment provided by engineer. */
+"New Drive" = "新建驱动器";
+
+/* No comment provided by engineer. */
+"New from template…" = "从此模板新建…";
+
+/* No comment provided by engineer. */
+"New Shared Directory…" = "新建共享目錄…";
+
+/* No comment provided by engineer. */
+"New VM" = "新建虚拟机";
+
+/* No comment provided by engineer. */
+"Older versions of UTM added each IDE device to a separate bus. Check this to change the configuration to place two units on each bus." = "旧版本的 UTM 会将每个 IDE 设备添加到单独的总线上。选中此项可更改配置,以在每条总线上放置两个设备。";
+
+/* No comment provided by engineer. */
+"Only available if host architecture matches the target. Otherwise, TCG emulation is used." = "仅当主机架构和目标匹配时才可用。否则将使用 TCG 模拟。";
+
+/* No comment provided by engineer. */
+"Only available on macOS virtual machines." = "仅限 macOS 虚拟机可用。";
+
+/* No comment provided by engineer. */
+"Only available when Hypervisor is used on supported hardware. TSO speeds up Intel emulation in the guest at the cost of decreased performance in general." = "只有在支持的硬件上使用 Hypervisor 时才可用。TSO 加快了客户机中的 Intel 仿真速度,但总体性能却有所下降。";
+
+/* No comment provided by engineer. */
+"Open VM Settings" = "打开虚拟机设置";
+
+/* No comment provided by engineer. */
+"Optionally select a directory to make accessible inside the VM. Note that support for shared directories varies by the guest operating system and may require additional guest drivers to be installed. See UTM support pages for more details." = "(可选) 选择一个可在虚拟机内访问的目录。请注意,客户操作系统对共享目录的支持各不相同,可能需要安装额外的客户驱动程序。有关详细信息,请参阅 UTM 支持页面。";
+
+/* No comment provided by engineer. */
+"Options here only apply on next boot and are not saved." = "此处的选项只在下次启动时生效,不会保存。";
+
+/* No comment provided by engineer. */
+"Path" = "路径";
+
+/* No comment provided by engineer. */
+"Port" = "端口";
+
+/* No comment provided by engineer. */
+"Power Off" = "关闭电源";
+
+/* No comment provided by engineer. */
+"Prompt" = "提示";
+
+/* No comment provided by engineer. */
+"Protocol" = "协议";
+
+/* No comment provided by engineer. */
+"QEMU Machine Properties" = "QEMU 虚拟机属性";
+
+/* No comment provided by engineer. */
+"Quit" = "退出";
+
+/* No comment provided by engineer. */
+"RAM" = "内存";
+
+/* No comment provided by engineer. */
+"Ramdisk (optional)" = "Ramdisk (选填)";
+
+/* No comment provided by engineer. */
+"Random" = "随机";
+
+/* No comment provided by engineer. */
+"Read Only?" = "只读?";
+
+/* No comment provided by engineer. */
+"Reclaim disk space by re-converting the disk image." = "通过重新转换来回收磁盘空间。";
+
+/* No comment provided by engineer. */
+"Reclaim Space" = "释放空间";
+
+/* No comment provided by engineer. */
+"Remove selected shortcut" = "移除选中的快捷方式";
+
+/* No comment provided by engineer. */
+"Renderer Backend" = "渲染器后端";
+
+/* No comment provided by engineer. */
+"Requires restarting UTM to take affect." = "需要重新打开 UTM 以生效。";
+
+/* No comment provided by engineer. */
+"Requires SPICE guest agent tools to be installed." = "需要安装 SPICE 客户机代理工具。";
+
+/* No comment provided by engineer. */
+"Reset UEFI Variables" = "重设 UEFI 变量";
+
+/* No comment provided by engineer. */
+"Resize Console Command" = "调整控制台大小指令";
+
+/* No comment provided by engineer. */
+"Resize…" = "调整大小…";
+
+/* No comment provided by engineer. */
+"Resizing is experimental and could result in data loss. You are strongly encouraged to back-up this VM before proceeding. Would you like to resize to %lld GiB?" = "调整大小是实验性功能,可能会导致数据丢失。强烈建议你在继续操作前备份此虚拟机。要将大小调整为 %lld GiB 吗?";
+
+/* No comment provided by engineer. */
+"Resolution" = "分辨率";
+
+/* No comment provided by engineer. */
+"Restart" = "重新启动";
+
+/* No comment provided by engineer. */
+"Resume" = "继续";
+
+/* No comment provided by engineer. */
+"Resume running VM." = "继续正在运行的虚拟机。";
+
+/* No comment provided by engineer. */
+"Reveal where the VM is stored." = "显示虚拟机的存储位置。";
+
+/* No comment provided by engineer. */
+"RNG Device" = "RNG 设备";
+
+/* No comment provided by engineer. */
+"Root Image" = "Root 映像";
+
+/* No comment provided by engineer. */
+"Run" = "运行";
+
+/* No comment provided by engineer. */
+"Run Recovery" = "运行 Recovery 模式";
+
+/* No comment provided by engineer. */
+"Run selected VM" = "运行已选中的虚拟机";
+
+/* No comment provided by engineer. */
+"Run the VM in the foreground." = "在前台运行虚拟机。";
+
+/* No comment provided by engineer. */
+"Run the VM in the foreground, without saving data changes to disk." = "在前台运行虚拟机,但不会将数据存储到磁盘。";
+
+/* No comment provided by engineer. */
+"Run without saving changes" = "运行但不存储更改";
+
+/* No comment provided by engineer. */
+"Section" = "区域";
+
+/* No comment provided by engineer. */
+"Secure Boot with TPM 2.0" = "使用 TPM 2.0 的安全启动";
+
+/* No comment provided by engineer. */
+"Select an existing disk image." = "选择一个现有的磁盘映像。";
+
+/* No comment provided by engineer. */
+"Serial" = "序列";
+
+/* No comment provided by engineer. */
+"Server Address" = "服务器地址";
+
+/* No comment provided by engineer. */
+"Settings" = "设置";
+
+/* No comment provided by engineer. */
+"Share" = "共享";
+
+/* No comment provided by engineer. */
+"Share…" = "共享…";
+
+/* No comment provided by engineer. */
+"Share a copy of this VM and all its data." = "共享此虚拟机及其所有数据的副本。";
+
+/* No comment provided by engineer. */
+"Share Directory" = "共享目录";
+
+/* No comment provided by engineer. */
+"Share is read only" = "共享为只读";
+
+/* No comment provided by engineer. */
+"Share selected VM" = "共享已选中的虚拟机";
+
+/* No comment provided by engineer. */
+"Shared Directory Path" = "共享目录路径";
+
+/* No comment provided by engineer. */
+"Shared Path" = "共享路径";
+
+/* No comment provided by engineer. */
+"Should be off for older operating systems such as Windows 7 or lower." = "对于较旧的操作系统应关闭此选项,例如 Windows 7 或者更低版本。";
+
+/* No comment provided by engineer. */
+"Should be on always unless the guest cannot boot because of this." = "除非客户机因此无法启动,否则此项应始终打开。";
+
+/* No comment provided by engineer. */
+"Show all devices…" = "显示所有设备…";
+
+/* No comment provided by engineer. */
+"Show in Finder" = "在访达中显示";
+
+/* No comment provided by engineer. */
+"Show the main window." = "显示主窗口。";
+
+/* No comment provided by engineer. */
+"Show UTM" = "显示 UTM";
+
+/* No comment provided by engineer. */
+"Show UTM preferences" = "显示 UTM 偏好设置";
+
+/* No comment provided by engineer. */
+"Skip Boot Image" = "跳过启动映像";
+
+/* No comment provided by engineer. */
+"Skip ISO boot" = "跳过 ISO 启动";
+
+/* No comment provided by engineer. */
+"Some older systems do not support UEFI boot, such as Windows 7 and below." = "一些旧版系统不支持 UEFI 启动,例如 Windows 7 及更低版本。";
+
+/* No comment provided by engineer. */
+"Sound" = "声音";
+
+/* No comment provided by engineer. */
+"Sound Backend" = "声音后端";
+
+/* No comment provided by engineer. */
+"Start" = "开始";
+
+/* No comment provided by engineer. */
+"Status" = "状态";
+
+/* No comment provided by engineer. */
+"Stop selected VM" = "停止已选中的虚拟机";
+
+/* No comment provided by engineer. */
+"Stop the running VM." = "停止正在运行的虚拟机。";
+
+/* No comment provided by engineer. */
+"Storage" = "存储空间";
+
+/* No comment provided by engineer. */
+"stty cols $COLS rows $ROWS\n" = "stty 行 $ROWS 列 $COLS\n";
+
+/* No comment provided by engineer. */
+"Suspend" = "挂起";
+
+/* No comment provided by engineer. */
+"Target" = "目标";
+
+/* No comment provided by engineer. */
+"Terminate UTM and stop all running VMs." = "终止 UTM 并停止所有正在运行的虚拟机。";
+
+/* No comment provided by engineer. */
+"Text" = "文字";
+
+/* No comment provided by engineer. */
+"Text Color" = "文字颜色";
+
+/* No comment provided by engineer. */
+"The amount of storage to allocate for this image. Ignored if importing an image. If this is a raw image, then an empty file of this size will be stored with the VM. Otherwise, the disk image will dynamically expand up to this size." = "为此映像分配的存储空间。在导入映像时,此参数会被忽略。若导入的是 Raw 映像,则虚拟机将存储一个与此相同大小的空文件。否则,磁盘映像将动态扩展至此大小。";
+
+/* No comment provided by engineer. */
+"Theme" = "主题";
+
+/* No comment provided by engineer. */
+"There are known issues in some newer Linux drivers including black screen, broken compositing, and apps failing to render." = "一些较新的 Linux 驱动程序存在已知问题,包括黑屏、合成失败和应用程序无法渲染。";
+
+/* No comment provided by engineer. */
+"These are advanced settings affecting QEMU which should be kept default unless you are running into issues." = "这些属于 QEMU 的高级设置,除非遇到问题,否则应当保持默认值。";
+
+/* No comment provided by engineer. */
+"This is appended to the -machine argument." = "这会添加到 -machine 参数的末端。";
+
+/* No comment provided by engineer. */
+"This virtual machine cannot be found at: %@" = "虚拟机无法在此路径中找到:%@";
+
+/* No comment provided by engineer. */
+"This virtual machine must be re-added to UTM by opening it with Finder. You can find it at the path: %@" = "必须使用访达打开此虚拟机,将其重新添加到 UTM 中。你可以在此路径中找到:%@";
+
+/* No comment provided by engineer. */
+"TPM 2.0 Device" = "TPM 2.0 设备";
+
+/* No comment provided by engineer. */
+"TPM can be used to protect secrets in the guest operating system. Note that the host will always be able to read these secrets and therefore no expectation of physical security is provided." = "TPM 可用于保护客户机操作系统中的机密。需要注意的是,主机始终可以读取这些机密,因此无法提供预期的物理安全性。";
+
+/* UTMAppleConfigurationDevices */
+"Trackpad" = "触控板";
+
+/* No comment provided by engineer. */
+"Tweaks" = "调整";
+
+/* No comment provided by engineer. */
+"Ubuntu Install Guide" = "Ubuntu 安装指南";
+
+/* No comment provided by engineer. */
+"UEFI Boot" = "UEFI 启动";
+
+/* No comment provided by engineer. */
+"Upscaling" = "粗化";
+
+/* No comment provided by engineer. */
+"USB Support" = "USB 支持";
+
+/* No comment provided by engineer. */
+"Use Apple Virtualization" = "使用 Apple 虚拟化";
+
+/* No comment provided by engineer. */
+"Use Hypervisor" = "使用 Hypervisor";
+
+/* No comment provided by engineer. */
+"Use local time for base clock" = "使用本地时间作为基本时钟";
+
+/* No comment provided by engineer. */
+"Use Rosetta" = "使用 Rosetta";
+
+/* No comment provided by engineer. */
+"Use Trackpad" = "使用触控板";
+
+/* No comment provided by engineer. */
+"Use TSO" = "使用 TSO";
+
+/* No comment provided by engineer. */
+"Use Virtualization" = "使用虚拟化";
+
+/* No comment provided by engineer. */
+"VGA Device RAM (MB)" = "VGA 设备内存 (MB)";
+
+/* No comment provided by engineer. */
+"Virtualization" = "虚拟化";
+
+/* No comment provided by engineer. */
+"Virtualization Engine" = "虚拟化引擎";
+
+/* No comment provided by engineer. */
+"Wait for Connection" = "等待连接";
+
+/* No comment provided by engineer. */
+"WebDAV requires installing SPICE daemon. VirtFS requires installing device drivers." = "WebDAV 需要安装 SPICE 守护程序。VirtFS 需要安装设备驱动程序。";
+
+/* No comment provided by engineer. */
+"Width" = "宽度";
+
+/* No comment provided by engineer. */
+"Windows Install Guide" = "Windows 安装指南";
+
+/* No comment provided by engineer. */
+"You can use this if your boot options are corrupted or if you wish to re-enroll in the default keys for secure boot." = "若启动选项已损坏,或者希望重新注册安全启动的默认密钥,可以使用此功能。";
+
+/* No comment provided by engineer. */
+"Zoom" = "缩放";
+
diff --git a/QEMUHelper/it.lproj/InfoPlist.strings b/QEMUHelper/it.lproj/InfoPlist.strings
new file mode 100644
index 000000000..d3205b168
--- /dev/null
+++ b/QEMUHelper/it.lproj/InfoPlist.strings
@@ -0,0 +1,9 @@
+/* Bundle display name */
+"CFBundleDisplayName" = "QEMUHelper";
+
+/* Bundle name */
+"CFBundleName" = "QEMUHelper";
+
+/* Copyright (human-readable) */
+"NSHumanReadableCopyright" = "Copyright © 2020 osy. Tutti i diritti riservati.";
+
diff --git a/QEMUHelper/it.lproj/Localizable.strings b/QEMUHelper/it.lproj/Localizable.strings
new file mode 100644
index 000000000..fccbdfd04
--- /dev/null
+++ b/QEMUHelper/it.lproj/Localizable.strings
@@ -0,0 +1,9 @@
+/* QEMUHelper */
+"Cannot find QEMU support libraries." = "Impossibile trovare le librerie di supporto di QEMU";
+
+/* QEMUHelper */
+"Error starting QEMU." = "Erorre durante l'avvio di QUEMU";
+
+/* QEMUHelper */
+"QEMU exited unexpectedly." = "QEMU si è interrotto inaspettatamente";
+
diff --git a/README.ko.md b/README.ko.md
new file mode 100644
index 000000000..7980a72fc
--- /dev/null
+++ b/README.ko.md
@@ -0,0 +1,78 @@
+# UTM
+[![Build](https://github.com/utmapp/UTM/workflows/Build/badge.svg?branch=master&event=push)][1]
+
+> 계산 가능한 수열을 계산하는 단일 기계를 발명할 수 있습니다.(It is possible to invent a single machine which can be used to compute any computable sequence.)
+
+-- 엘런 튜링, 1936
+
+UTM은 iOS와 macOS를 위한 완전한 시스템 에뮬레이터, 가상머신입니다. 이것은 QEMU를 기반으로 합니다. 요컨데 당신은 이것을 통해, Windows나 Linux와 같은 운영체제들을 Mac, iPhone, iPad 등에서 구동할 수 있습니다. 자세한 내용은 https://getutm.app/ 와 https://mac.getutm.app/ 를 읽어주세요.
+
+
+
+
+
+
+
+## 주요기능
+
+* QMEU를 활용한 완전한 시스템 에뮬레이션(MMU, 기타 기기들)
+* x86_64, ARM64, and RISC-V를 포함한 30가지 이상의 프로세서 지원
+* SPICE와 QXL을 활용한 VGA 그래픽 모드
+* 텍스트 터미널 모드
+* USB 장치들
+* QEMU TCG를 활용한 JIT 기반 가속
+* 초기부터 macOS 11과 iOS 11+를 위해 디자인된, 최신 및 최고의 API를 활용한 프론트엔드
+* 당신의 기기에서 바로 가상머신을 생성하고, 관리하고, 구동하기
+
+## macOS 추가 기능
+
+* Hypervisor.framework와 QEMU를 활용한 하드웨어 가속 가상화
+* macOS 12+에서 Virtualization.framework를 통해 macOS 게스트 구동
+
+## UTM SE
+
+UTM/QEMU이 최고의 성능을 내기 위해서는 동적 코드 생성이(JIT) 필요합니다. iOS 기기에서 JIT는 jailbroken를 요구하거나, 특정 iOS 버전에서 발견된 다양한 해결책 중 하나를 필요로 합니다.(자세한 내용은 "설치" 부분을 참고해주세요."
+
+UTM SE("slow edition")은 [threaded interpreter][3]를 사용합니다. 이는 전통적인 인터프리터보다는 좋지만, 그래도 여전히 JIT보다는 느립니다. 이 기술은 [iSH][4]가 동적 실행을 위해 하는 일과 유사한데요. 결과적으로 UTM SE는 탈옥이나 JIT 해결책을 요구하진 않고, 정규 앱으로 나란히 메모리에 적재될 수 있습니다.
+
+빌드 시간과 크기를 최적하기 위해서, UTM SE에는 ARM, PPC, RISC-V, x86(32bit와 64bit 변종 모두) 아키텍처들만이 포함되어 있습니다.
+
+## 설치
+
+iOS를 위한 UTM (SE): https://getutm.app/install/
+
+macOS를 위한 UTM: https://mac.getutm.app/
+
+## 개발
+
+### [macOS 개발](Documentation/MacDevelopment.md)
+
+### [iOS 개발](Documentation/iOSDevelopment.md)
+
+## 관련사항
+
+* [iSH][4]: iOS에서 x86 Linux 앱을 실행하기 위해, 사용자 모드 Linux 터미널 인터페이스를 에뮬레이트
+* [a-shell][5]: 기본적으로 iOS용으로 구축되면서, 터미널 인터페이스를 통해 액세스할 수 있는, 범용 유닉스 명령 및 유틸리티 패키지
+
+## 라이센스
+
+UTM은 permissive Apache 2.0 license를 따르며 배포되었습니다. 하지만 몇몇 (L)GPL 컴포넌트들을 사용하는데요. 대부분은 동적으로 연결되어있지만, gstreamer 플러그인은 정적으로 연결되어 있고, 일부 코드는 qemu에서 가져왔습니다. 이 앱을 재배포 하려는 경우 꼭 이에 유의하시길 바랍니다.
+
+일부 아이콘은 [www.flaticon.com](https://www.flaticon.com/)에서 [Freepik](https://www.freepik.com)를 통해 만들어졌습니다.
+
+추가적으로 UTM 프론트엔드는 아래의 MIT/BSD 라이센스를 사용하는 컴포넌트들에 의존하고 있습니다.
+
+* [IQKeyboardManager](https://github.com/hackiftekhar/IQKeyboardManager)
+* [SwiftTerm](https://github.com/migueldeicaza/SwiftTerm)
+* [ZIP Foundation](https://github.com/weichsel/ZIPFoundation)
+* [InAppSettingsKit](https://github.com/futuretap/InAppSettingsKit)
+
+지속 통합 호스팅은 다음을 통해 제공됩니다. [MacStadium](https://www.macstadium.com/opensource)
+
+[](https://www.macstadium.com)
+
+ [1]: https://github.com/utmapp/UTM/actions?query=event%3Arelease+workflow%3ABuild
+ [2]: screen.png
+ [3]: https://github.com/ktemkin/qemu/blob/with_tcti/tcg/aarch64-tcti/README.md
+ [4]: https://github.com/ish-app/ish
+ [5]: https://github.com/holzschu/a-shell
diff --git a/README.zh-HK.md b/README.zh-HK.md
index 1c4b4f063..e13e251ae 100644
--- a/README.zh-HK.md
+++ b/README.zh-HK.md
@@ -4,7 +4,7 @@
> 發明一台可用於計算任何可計算序列的機器是可行的。
-- 艾倫·圖靈(Alan Turing), 1936 年
-UTM 是一個功能完备的系統模擬工具和虛擬电脑主機,適用於 iOS 和 macOS。它以 QEMU 為基礎。簡言之,它允許你在 Mac、iPhone 和 iPad 上執行 Windows、Linux 等。更多訊息請參閱 https://getutm.app/ 與 https://mac.getutm.app/。
+UTM 是一個功能完备的系統模擬工具與虛擬电脑主機,適用於 iOS 和 macOS。它基於 QEMU。簡言之,它允許你在 Mac、iPhone 和 iPad 上執行 Windows、Linux 等。更多訊息請見 https://getutm.app/ 與 https://mac.getutm.app/。
@@ -14,7 +14,7 @@ UTM 是一個功能完备的系統模擬工具和虛擬电脑主機,適用於
## 特性
-* 使用 QEMU 進行全作業系統模擬(MMU、設備等)
+* 使用 QEMU 進行全作業系統模擬(MMU、裝置等)
* 支援逾三十種體系結構 CPU,包括 x86_64、ARM64 和 RISC-V
* 使用 SPICE 與 QXL 的 VGA 圖形模式
* 文本終端機模式
@@ -23,14 +23,14 @@ UTM 是一個功能完备的系統模擬工具和虛擬电脑主機,適用於
* 採用了最新最靚的 API,從零開始設計前端,支援 macOS 11+ 與 iOS 11+
* 從你的裝置上直接製作、管理和執行虛擬機
-## macOS 的附加功能
+## 於 macOS 的附加功能
* 使用 Hypervisor.framework 與 QEMU 實現硬件加速虛擬化
* 在 macOS 12+ 上使用 Virtualization.framework 來啓動 macOS 客戶端
## UTM SE
-UTM/QEMU 需要動態程式碼生成(JIT)以得到最大性能。iOS 上的 JIT 需要已經越獄(Jailbreak)的裝置(iOS 11.0~14.3 無需越獄,iOS 14.4+ 需要),或者為特定版本的 iOS 找到其他變通方法之一(有關更多詳細訊息,請參閱「安裝」)。
+UTM/QEMU 需要動態程式碼生成(JIT)以得到最大性能。iOS 上的 JIT 需要已經越獄(Jailbreak)的裝置(iOS 11.0~14.3 毋需越獄,iOS 14.4+ 需要),或者為特定版本的 iOS 找到其他變通方法之一(有關更多詳細訊息,請見「安裝」)。
UTM SE(「較慢版」)使用了「[執行緒解釋器][3]」,其性能優於傳統解釋器,但仍然比 JIT 要慢。此種技術類似於 [iSH][4] 的動態執行。因此,UTM SE 無需越獄或任何 JIT 的變通方法,可以作為常規應用程式側載(Sideload)。
@@ -55,7 +55,7 @@ UTM 同時支援 macOS:https://mac.getutm.app/
## 許可證
-UTM 於 Apache 2.0 許可證下發佈,但它採用了若干 GPL 與 LGPL 元件。這其中,大多數元件是動態連接的,但 gstreamer 元件是靜態連接的,部分程式碼來自 QEMU。如果你打算重新分發此應用程式,請務必謹記這一點。
+UTM 於 Apache 2.0 許可證下發佈,但它採用了若干 GPL 與 LGPL 元件。這其中,大多數元件為動態連接,但 gstreamer 元件為靜態連接,部分程式碼來自 QEMU。如你打算重新分發此應用程式,請務必緊記這一點。
某些图示由 [Freepik](https://www.freepik.com) 從 [www.flaticon.com](https://www.flaticon.com/) 製作。
diff --git a/Remote/GenerateKey.c b/Remote/GenerateKey.c
new file mode 100644
index 000000000..f7bad39ac
--- /dev/null
+++ b/Remote/GenerateKey.c
@@ -0,0 +1,276 @@
+//
+// Copyright © 2023 osy. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+#include "GenerateKey.h"
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#define X509_ENTRY_MAX_LENGTH (1024)
+
+/* Add extension using V3 code: we can set the config file as NULL
+ * because we wont reference any other sections.
+ */
+static int add_ext(X509 *cert, int nid, char *value) {
+ X509_EXTENSION *ex;
+ X509V3_CTX ctx;
+ /* This sets the 'context' of the extensions. */
+ /* No configuration database */
+ X509V3_set_ctx_nodb(&ctx);
+ /* Issuer and subject certs: both the target since it is self signed,
+ * no request and no CRL
+ */
+ X509V3_set_ctx(&ctx, cert, cert, NULL, NULL, 0);
+ ex = X509V3_EXT_conf_nid(NULL, &ctx, nid, value);
+ if (!ex) {
+ return 0;
+ }
+
+ X509_add_ext(cert, ex, -1);
+ X509_EXTENSION_free(ex);
+ return 1;
+}
+
+static int mkrsacert(X509 **x509p, EVP_PKEY **pkeyp, const char *commonName, const char *organizationName, long serial, int days, int isClient) {
+ X509 *x = NULL;
+ EVP_PKEY *pk = NULL;
+ BIGNUM *bne = NULL;
+ RSA *rsa = NULL;
+ X509_NAME *name = NULL;
+
+ if ((pk = EVP_PKEY_new()) == NULL) {
+ goto err;
+ }
+
+ if ((x = X509_new()) == NULL) {
+ goto err;
+ }
+
+ bne = BN_new();
+ if (!bne || !BN_set_word(bne, RSA_F4)){
+ goto err;
+ }
+
+ rsa = RSA_new();
+ if (!rsa || !RSA_generate_key_ex(rsa, 4096, bne, NULL)) {
+ goto err;
+ }
+ BN_free(bne);
+ bne = NULL;
+ if (!EVP_PKEY_assign_RSA(pk, rsa)) {
+ goto err;
+ }
+ rsa = NULL; // EVP_PKEY_assign_RSA takes ownership
+
+ X509_set_version(x, 2);
+ ASN1_INTEGER_set(X509_get_serialNumber(x), serial);
+ X509_gmtime_adj(X509_get_notBefore(x), 0);
+ X509_gmtime_adj(X509_get_notAfter(x), (long)60*60*24*days);
+ X509_set_pubkey(x, pk);
+
+ name = X509_get_subject_name(x);
+
+ /* This function creates and adds the entry, working out the
+ * correct string type and performing checks on its length.
+ * Normally we'd check the return value for errors...
+ */
+ X509_NAME_add_entry_by_txt(name, SN_commonName,
+ MBSTRING_UTF8, (const unsigned char *)commonName, -1, -1, 0);
+ X509_NAME_add_entry_by_txt(name, SN_organizationName,
+ MBSTRING_UTF8, (const unsigned char *)organizationName, -1, -1, 0);
+
+ /* Its self signed so set the issuer name to be the same as the
+ * subject.
+ */
+ X509_set_issuer_name(x, name);
+
+ /* Add various extensions: standard extensions */
+ add_ext(x, NID_basic_constraints, "critical,CA:TRUE");
+ add_ext(x, NID_key_usage, "critical,keyCertSign,cRLSign,keyEncipherment,digitalSignature");
+ if (isClient) {
+ add_ext(x, NID_ext_key_usage, "clientAuth");
+ } else {
+ add_ext(x, NID_ext_key_usage, "serverAuth");
+ }
+ add_ext(x, NID_subject_key_identifier, "hash");
+
+ if (!X509_sign(x, pk, EVP_sha256())) {
+ goto err;
+ }
+
+ *x509p = x;
+ *pkeyp = pk;
+ return 1;
+err:
+ if (pk) {
+ EVP_PKEY_free(pk);
+ }
+ if (x) {
+ X509_free(x);
+ }
+ if (bne) {
+ BN_free(bne);
+ }
+ return 0;
+}
+
+static _Nullable CFDataRef CreateP12FromKey(EVP_PKEY *pkey, X509 *cert) {
+ PKCS12 *p12;
+ BIO *mem;
+ char *ptr;
+ long length;
+ CFDataRef data;
+
+ p12 = PKCS12_create("password", NULL, pkey, cert, NULL, NID_pbe_WithSHA1And3_Key_TripleDES_CBC, NID_pbe_WithSHA1And40BitRC2_CBC, PKCS12_DEFAULT_ITER, 1, 0);
+ if (!p12) {
+ ERR_print_errors_fp(stderr);
+ return NULL;
+ }
+ mem = BIO_new(BIO_s_mem());
+ if (!mem || !i2d_PKCS12_bio(mem, p12)) {
+ ERR_print_errors_fp(stderr);
+ PKCS12_free(p12);
+ BIO_free(mem);
+ return NULL;
+ }
+ PKCS12_free(p12);
+ length = BIO_get_mem_data(mem, &ptr);
+ data = CFDataCreate(kCFAllocatorDefault, (void *)ptr, length);
+ BIO_free(mem);
+ return data;
+}
+
+static _Nullable CFDataRef CreatePrivatePEMFromKey(EVP_PKEY *pkey) {
+ BIO *mem;
+ char *ptr;
+ long length;
+ CFDataRef data;
+
+ mem = BIO_new(BIO_s_mem());
+ if (!mem || !PEM_write_bio_PrivateKey(mem, pkey, NULL, NULL, 0, NULL, NULL)) {
+ ERR_print_errors_fp(stderr);
+ BIO_free(mem);
+ return NULL;
+ }
+ length = BIO_get_mem_data(mem, &ptr);
+ data = CFDataCreate(kCFAllocatorDefault, (void *)ptr, length);
+ BIO_free(mem);
+ return data;
+}
+
+static _Nullable CFDataRef CreatePublicPEMFromCert(X509 *cert) {
+ BIO *mem;
+ char *ptr;
+ long length;
+ CFDataRef data;
+
+ mem = BIO_new(BIO_s_mem());
+ if (!mem || !PEM_write_bio_X509(mem, cert)) {
+ ERR_print_errors_fp(stderr);
+ BIO_free(mem);
+ return NULL;
+ }
+ length = BIO_get_mem_data(mem, &ptr);
+ data = CFDataCreate(kCFAllocatorDefault, (void *)ptr, length);
+ BIO_free(mem);
+ return data;
+}
+
+static _Nullable CFDataRef CreatePublicKeyFromCert(X509 *cert) {
+ EVP_PKEY* pubkey;
+ BIO *mem;
+ char *ptr;
+ long length;
+ CFDataRef data;
+
+ pubkey = X509_get_pubkey(cert);
+ if (!pubkey) {
+ ERR_print_errors_fp(stderr);
+ return NULL;
+ }
+ mem = BIO_new(BIO_s_mem());
+ if (!mem || !i2d_PUBKEY_bio(mem, pubkey)) {
+ ERR_print_errors_fp(stderr);
+ EVP_PKEY_free(pubkey);
+ BIO_free(mem);
+ return NULL;
+ }
+ length = BIO_get_mem_data(mem, &ptr);
+ data = CFDataCreate(kCFAllocatorDefault, (void *)ptr, length);
+ BIO_free(mem);
+ EVP_PKEY_free(pubkey);
+ return data;
+}
+
+_Nullable CFArrayRef GenerateRSACertificate(CFStringRef _Nonnull commonName, CFStringRef _Nonnull organizationName, CFNumberRef _Nullable serial, CFNumberRef _Nullable days, CFBooleanRef _Nonnull isClient) {
+ char _commonName[X509_ENTRY_MAX_LENGTH];
+ char _organizationName[X509_ENTRY_MAX_LENGTH];
+ long _serial = 0;
+ int _days = 365;
+ int _isClient = 0;
+ X509 *cert;
+ EVP_PKEY *pkey;
+ CFDataRef arr[4] = {NULL};
+ CFArrayRef cfarr = NULL;
+
+ if (!CFStringGetCString(commonName, _commonName, X509_ENTRY_MAX_LENGTH, kCFStringEncodingUTF8)) {
+ return NULL;
+ }
+ if (!CFStringGetCString(organizationName, _organizationName, X509_ENTRY_MAX_LENGTH, kCFStringEncodingUTF8)) {
+ return NULL;
+ }
+ if (serial) {
+ CFNumberGetValue(serial, kCFNumberLongType, &_serial);
+ }
+ if (days) {
+ CFNumberGetValue(days, kCFNumberIntType, &_days);
+ }
+ _isClient = CFBooleanGetValue(isClient);
+
+ OpenSSL_add_all_algorithms();
+ ERR_load_crypto_strings();
+ if (!mkrsacert(&cert, &pkey, _commonName, _organizationName, _serial, _days, _isClient)) {
+ ERR_print_errors_fp(stderr);
+ return NULL;
+ }
+ arr[0] = CreateP12FromKey(pkey, cert);
+ arr[1] = CreatePrivatePEMFromKey(pkey);
+ arr[2] = CreatePublicPEMFromCert(cert);
+ arr[3] = CreatePublicKeyFromCert(cert);
+ if (arr[0] && arr[1] && arr[2] && arr[3]) {
+ cfarr = CFArrayCreate(kCFAllocatorDefault, (const void **)arr, 4, &kCFTypeArrayCallBacks);
+ }
+ if (arr[0]) {
+ CFRelease(arr[0]);
+ }
+ if (arr[1]) {
+ CFRelease(arr[1]);
+ }
+ if (arr[2]) {
+ CFRelease(arr[2]);
+ }
+ if (arr[3]) {
+ CFRelease(arr[3]);
+ }
+ EVP_PKEY_free(pkey);
+ X509_free(cert);
+ return cfarr;
+}
diff --git a/Remote/GenerateKey.h b/Remote/GenerateKey.h
new file mode 100644
index 000000000..7e73d5f27
--- /dev/null
+++ b/Remote/GenerateKey.h
@@ -0,0 +1,33 @@
+//
+// Copyright © 2023 osy. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+#ifndef GenerateKey_h
+#define GenerateKey_h
+
+#include
+
+/// Generate a RSA-4096 key and return a PKCS#12 encoded data
+///
+/// The password of the blob is `password`. Returns NULL on error.
+/// - Parameters:
+/// - commonName: CN field of the certificate, max length is 1024 bytes
+/// - organizationName: O field of the certificate, max length is 1024 bytes
+/// - serial: Serial number of the certificate
+/// - days: Validity in days from today
+/// - isClient: If 0 then a TLS Server certificate is generated, otherwise a TLS Client certificate is generated
+_Nullable CFArrayRef GenerateRSACertificate(CFStringRef _Nonnull commonName, CFStringRef _Nonnull organizationName, CFNumberRef _Nullable serial, CFNumberRef _Nullable days, CFBooleanRef _Nonnull isClient);
+
+#endif /* GenerateKey_h */
diff --git a/Remote/UTMRemoteClient.swift b/Remote/UTMRemoteClient.swift
new file mode 100644
index 000000000..f2cc3d673
--- /dev/null
+++ b/Remote/UTMRemoteClient.swift
@@ -0,0 +1,588 @@
+//
+// Copyright © 2024 osy. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+import Foundation
+import Network
+import SwiftConnect
+
+let service = "_utm_server._tcp"
+
+actor UTMRemoteClient {
+ let state: State
+ private let keyManager = UTMRemoteKeyManager(forClient: true)
+ private let connectionQueue = DispatchQueue(label: "UTM Remote Client Connection")
+ private var local: Local
+
+ private var scanTask: Task?
+
+ private(set) var server: Remote!
+
+ nonisolated var fingerprint: [UInt8] {
+ keyManager.fingerprint ?? []
+ }
+
+ @MainActor
+ init(data: UTMRemoteData) {
+ self.state = State()
+ self.local = Local(data: data)
+ }
+
+ private func withErrorAlert(_ body: () async throws -> Void) async {
+ do {
+ try await body()
+ } catch {
+ await state.showErrorAlert(error.localizedDescription)
+ }
+ }
+
+ func startScanning() {
+ scanTask = Task {
+ await withErrorAlert {
+ for try await results in Connection.browse(forServiceType: service) {
+ await self.didFindResults(results)
+ }
+ }
+ }
+ }
+
+ func stopScanning() {
+ scanTask?.cancel()
+ scanTask = nil
+ }
+
+ func refresh() {
+ stopScanning()
+ startScanning()
+ }
+
+ func didFindResults(_ results: Set) async {
+ let servers = results.compactMap { result in
+ let model: String?
+ if case .bonjour(let txtRecord) = result.metadata,
+ case .string(let value) = txtRecord.getEntry(for: "Model") {
+ model = value
+ } else {
+ model = nil
+ }
+ switch result.endpoint {
+ case .service(let name, _, _, _):
+ return State.DiscoveredServer(hostname: result.endpoint.debugDescription, model: model, name: name, endpoint: result.endpoint)
+ default:
+ return nil
+ }
+ }
+ await state.updateFoundServers(servers)
+ }
+
+ func connect(_ server: State.SavedServer) async throws {
+ var isSuccessful = false
+ let endpoint = server.endpoint ?? NWEndpoint.hostPort(host: .init(server.hostname), port: .init(integerLiteral: UInt16(server.port ?? 0)))
+ try await keyManager.load()
+ let connection = try await Connection(endpoint: endpoint, connectionQueue: connectionQueue, identity: keyManager.identity) { connection, error in
+ Task {
+ do {
+ try await self.local.data.reconnect(to: server)
+ } catch {
+ // reconnect failed
+ await self.state.setConnected(false)
+ await self.state.showErrorAlert(error.localizedDescription)
+ }
+ }
+ }
+ defer {
+ if !isSuccessful {
+ connection.close()
+ }
+ }
+ guard let host = connection.connection.currentPath?.remoteEndpoint?.hostname else {
+ throw ConnectionError.cannotDetermineHost
+ }
+ guard let fingerprint = connection.peerCertificateChain.first?.fingerprint() else {
+ throw ConnectionError.cannotFindFingerprint
+ }
+ if server.fingerprint.isEmpty {
+ throw ConnectionError.fingerprintUntrusted(fingerprint)
+ } else if server.fingerprint != fingerprint {
+ throw ConnectionError.fingerprintMismatch(fingerprint)
+ }
+ try Task.checkCancellation()
+ let peer = Peer(connection: connection, localInterface: local)
+ let remote = Remote(peer: peer, host: host)
+ let (isAuthenticated, device) = try await remote.handshake(password: server.password)
+ if !isAuthenticated {
+ if server.password == nil {
+ throw ConnectionError.passwordRequired
+ } else {
+ throw ConnectionError.passwordInvalid
+ }
+ }
+ self.server = remote
+ var server = server
+ await state.setConnected(true)
+ if !server.shouldSavePassword {
+ server.password = nil
+ }
+ if server.name.isEmpty {
+ server.name = server.hostname
+ }
+ server.lastSeen = Date()
+ server.model = device.model
+ await state.save(server: server)
+ isSuccessful = true
+ }
+}
+
+extension UTMRemoteClient {
+ @MainActor
+ class State: ObservableObject {
+ typealias ServerFingerprint = [UInt8]
+
+ struct DiscoveredServer: Identifiable {
+ let hostname: String
+ var model: String?
+ var name: String
+ var endpoint: NWEndpoint
+
+ var id: String {
+ hostname
+ }
+ }
+
+ struct SavedServer: Codable, Identifiable {
+ var fingerprint: ServerFingerprint
+ var hostname: String
+ var port: Int?
+ var model: String?
+ var name: String
+ var lastSeen: Date
+ var password: String?
+ var endpoint: NWEndpoint?
+ var shouldSavePassword: Bool = false
+
+ private enum CodingKeys: String, CodingKey {
+ case fingerprint, hostname, port, model, name, lastSeen, password
+ }
+
+ var id: ServerFingerprint {
+ fingerprint
+ }
+
+ var isAvailable: Bool {
+ endpoint != nil || (port != nil && port != 0)
+ }
+
+ init() {
+ self.hostname = ""
+ self.name = ""
+ self.lastSeen = Date()
+ self.fingerprint = []
+ }
+
+ init(from discovered: DiscoveredServer) {
+ self.hostname = discovered.hostname
+ self.model = discovered.model
+ self.name = discovered.name
+ self.lastSeen = Date()
+ self.endpoint = discovered.endpoint
+ self.fingerprint = []
+ }
+ }
+
+ struct AlertMessage: Identifiable {
+ let id = UUID()
+ let message: String
+ }
+
+ @Published var savedServers: [SavedServer] {
+ didSet {
+ UserDefaults.standard.setValue(try! savedServers.propertyList(), forKey: "TrustedServers")
+ }
+ }
+
+ @Published var foundServers: [DiscoveredServer] = []
+
+ @Published var isScanning: Bool = false
+
+ @Published private(set) var isConnected: Bool = false
+
+ @Published var alertMessage: AlertMessage?
+
+ init() {
+ var _savedServers = Array()
+ if let array = UserDefaults.standard.array(forKey: "TrustedServers") {
+ if let servers = try? Array(fromPropertyList: array) {
+ _savedServers = servers
+ }
+ }
+ self.savedServers = _savedServers
+ }
+
+ func showErrorAlert(_ message: String) {
+ alertMessage = AlertMessage(message: message)
+ }
+
+ func updateFoundServers(_ servers: [DiscoveredServer]) {
+ for idx in savedServers.indices {
+ savedServers[idx].endpoint = nil
+ }
+ foundServers = servers.filter { server in
+ if let idx = savedServers.firstIndex(where: { $0.port == nil && $0.hostname == server.hostname }) {
+ savedServers[idx].endpoint = server.endpoint
+ return false
+ } else {
+ return true
+ }
+ }
+ }
+
+ func save(server: SavedServer) {
+ if let idx = savedServers.firstIndex(where: { $0.fingerprint == server.fingerprint }) {
+ savedServers[idx] = server
+ } else {
+ savedServers.append(server)
+ }
+ }
+
+ func delete(server: SavedServer) {
+ savedServers.removeAll(where: { $0.fingerprint == server.fingerprint })
+ }
+
+ fileprivate func setConnected(_ connected: Bool) {
+ isConnected = connected
+ }
+ }
+}
+
+extension UTMRemoteClient {
+ class Local: LocalInterface {
+ typealias M = UTMRemoteMessageClient
+
+ fileprivate let data: UTMRemoteData
+
+ init(data: UTMRemoteData) {
+ self.data = data
+ }
+
+ func handle(message: M, data: Data) async throws -> Data {
+ switch message {
+ case .clientHandshake:
+ return try await _handshake(parameters: .decode(data)).encode()
+ case .listHasChanged:
+ return try await _listHasChanged(parameters: .decode(data)).encode()
+ case .qemuConfigurationHasChanged:
+ return try await _qemuConfigurationHasChanged(parameters: .decode(data)).encode()
+ case .mountedDrivesHasChanged:
+ return try await _mountedDrivesHasChanged(parameters: .decode(data)).encode()
+ case .virtualMachineDidTransition:
+ return try await _virtualMachineDidTransition(parameters: .decode(data)).encode()
+ case .virtualMachineDidError:
+ return try await _virtualMachineDidError(parameters: .decode(data)).encode()
+ }
+ }
+
+ private func _handshake(parameters: M.ClientHandshake.Request) async throws -> M.ClientHandshake.Reply {
+ return .init(version: UTMRemoteMessageClient.version, capabilities: .current)
+ }
+
+ private func _listHasChanged(parameters: M.ListHasChanged.Request) async throws -> M.ListHasChanged.Reply {
+ await data.remoteListHasChanged(ids: parameters.ids)
+ return .init()
+ }
+
+ private func _qemuConfigurationHasChanged(parameters: M.QEMUConfigurationHasChanged.Request) async throws -> M.QEMUConfigurationHasChanged.Reply {
+ await data.remoteQemuConfigurationHasChanged(id: parameters.id, configuration: parameters.configuration)
+ return .init()
+ }
+
+ private func _mountedDrivesHasChanged(parameters: M.MountedDrivesHasChanged.Request) async throws -> M.MountedDrivesHasChanged.Reply {
+ await data.remoteMountedDrivesHasChanged(id: parameters.id, mountedDrives: parameters.mountedDrives)
+ return .init()
+ }
+
+ private func _virtualMachineDidTransition(parameters: M.VirtualMachineDidTransition.Request) async throws -> M.VirtualMachineDidTransition.Reply {
+ await data.remoteVirtualMachineDidTransition(id: parameters.id, state: parameters.state, isTakeoverAllowed: parameters.isTakeoverAllowed)
+ return .init()
+ }
+
+ private func _virtualMachineDidError(parameters: M.VirtualMachineDidError.Request) async throws -> M.VirtualMachineDidError.Reply {
+ await data.remoteVirtualMachineDidError(id: parameters.id, message: parameters.errorMessage)
+ return .init()
+ }
+ }
+}
+
+extension UTMRemoteClient {
+ class Remote {
+ typealias M = UTMRemoteMessageServer
+ private let peer: Peer
+ let host: String
+ private(set) var capabilities: UTMCapabilities?
+
+ init(peer: Peer, host: String) {
+ self.peer = peer
+ self.host = host
+ }
+
+ func close() {
+ peer.close()
+ }
+
+ func handshake(password: String?) async throws -> (isAuthenticated: Bool, device: MacDevice) {
+ let reply = try await _handshake(parameters: .init(version: UTMRemoteMessageServer.version, password: password))
+ guard reply.version == UTMRemoteMessageServer.version else {
+ throw ClientError.versionMismatch
+ }
+ capabilities = reply.capabilities
+ return (isAuthenticated: reply.isAuthenticated, device: MacDevice(model: reply.model))
+ }
+
+ func listVirtualMachines() async throws -> [UUID] {
+ try await _listVirtualMachines(parameters: .init()).ids
+ }
+
+ func reorderVirtualMachines(fromIds ids: [UUID], toOffset offset: Int) async throws {
+ try await _reorderVirtualMachines(parameters: .init(ids: ids, offset: offset))
+ }
+
+ func getVirtualMachineInformation(for ids: [UUID]) async throws -> [M.VirtualMachineInformation] {
+ try await _getVirtualMachineInformation(parameters: .init(ids: ids)).informations
+ }
+
+ func getQEMUConfiguration(for id: UUID) async throws -> UTMQemuConfiguration {
+ try await _getQEMUConfiguration(parameters: .init(id: id)).configuration
+ }
+
+ func getPackageSize(for id: UUID) async throws -> Int64 {
+ try await _getPackageSize(parameters: .init(id: id)).size
+ }
+
+ func getPackageFile(for id: UUID, relativePathComponents: [String]) async throws -> URL {
+ let fm = FileManager.default
+ let packageUrl = try packageUrl(for: id)
+ let fileUrl = packageUrl.appendingPathComponent(relativePathComponents.joined(separator: "_"))
+ var lastModified: Date?
+ if fm.fileExists(atPath: fileUrl.path) {
+ lastModified = try? fm.attributesOfItem(atPath: fileUrl.path)[.modificationDate] as? Date
+ }
+ let reply = try await _getPackageFile(parameters: .init(id: id, relativePathComponents: relativePathComponents, lastModified: lastModified))
+ if let data = reply.data {
+ fm.createFile(atPath: fileUrl.path, contents: data, attributes: [.modificationDate: reply.lastModified])
+ }
+ return fileUrl
+ }
+
+ func sendPackageFile(for id: UUID, relativePathComponents: [String], data: Data) async throws {
+ let fm = FileManager.default
+ let packageUrl = try packageUrl(for: id)
+ let fileUrl = packageUrl.appendingPathComponent(relativePathComponents.joined(separator: "_"))
+ guard fm.createFile(atPath: fileUrl.path, contents: data) else {
+ throw ConnectionError.failedToAccessFile
+ }
+ guard let lastModified = try fm.attributesOfItem(atPath: fileUrl.path)[.modificationDate] as? Date else {
+ throw ConnectionError.failedToAccessFile
+ }
+ try await _sendPackageFile(parameters: .init(id: id, relativePathComponents: relativePathComponents, lastModified: lastModified, data: data))
+ }
+
+ func deletePackageFile(for id: UUID, relativePathComponents: [String]) async throws {
+ let fm = FileManager.default
+ let packageUrl = try packageUrl(for: id)
+ let fileUrl = packageUrl.appendingPathComponent(relativePathComponents.joined(separator: "_"))
+ try fm.removeItem(at: fileUrl)
+ try await _deletePackageFile(parameters: .init(id: id, relativePathComponents: relativePathComponents))
+ }
+
+ func mountGuestToolsOnVirtualMachine(id: UUID) async throws {
+ try await _mountGuestToolsOnVirtualMachine(parameters: .init(id: id))
+ }
+
+ func startVirtualMachine(id: UUID, options: UTMVirtualMachineStartOptions) async throws -> UTMRemoteMessageServer.StartVirtualMachine.ServerInformation {
+ return try await _startVirtualMachine(parameters: .init(id: id, options: options)).serverInfo
+ }
+
+ func stopVirtualMachine(id: UUID, method: UTMVirtualMachineStopMethod) async throws {
+ try await _stopVirtualMachine(parameters: .init(id: id, method: method))
+ }
+
+ func restartVirtualMachine(id: UUID) async throws {
+ try await _restartVirtualMachine(parameters: .init(id: id))
+ }
+
+ func pauseVirtualMachine(id: UUID) async throws {
+ try await _pauseVirtualMachine(parameters: .init(id: id))
+ }
+
+ func resumeVirtualMachine(id: UUID) async throws {
+ try await _resumeVirtualMachine(parameters: .init(id: id))
+ }
+
+ func saveSnapshotVirtualMachine(id: UUID, name: String?) async throws {
+ try await _saveSnapshotVirtualMachine(parameters: .init(id: id, name: name))
+ }
+
+ func deleteSnapshotVirtualMachine(id: UUID, name: String?) async throws {
+ try await _deleteSnapshotVirtualMachine(parameters: .init(id: id, name: name))
+ }
+
+ func restoreSnapshotVirtualMachine(id: UUID, name: String?) async throws {
+ try await _restoreSnapshotVirtualMachine(parameters: .init(id: id, name: name))
+ }
+
+ func changePointerTypeVirtualMachine(id: UUID, toTabletMode tablet: Bool) async throws {
+ try await _changePointerTypeVirtualMachine(parameters: .init(id: id, isTabletMode: tablet))
+ }
+
+ private func packageUrl(for id: UUID) throws -> URL {
+ let fm = FileManager.default
+ let cacheUrl = try fm.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
+ let packageUrl = cacheUrl.appendingPathComponent(id.uuidString)
+ if !fm.fileExists(atPath: packageUrl.path) {
+ try fm.createDirectory(at: packageUrl, withIntermediateDirectories: false)
+ }
+ return packageUrl
+ }
+
+ private func _handshake(parameters: M.ServerHandshake.Request) async throws -> M.ServerHandshake.Reply {
+ try await M.ServerHandshake.send(parameters, to: peer)
+ }
+
+ private func _listVirtualMachines(parameters: M.ListVirtualMachines.Request) async throws -> M.ListVirtualMachines.Reply {
+ try await M.ListVirtualMachines.send(parameters, to: peer)
+ }
+
+ @discardableResult
+ private func _reorderVirtualMachines(parameters: M.ReorderVirtualMachines.Request) async throws -> M.ReorderVirtualMachines.Reply {
+ try await M.ReorderVirtualMachines.send(parameters, to: peer)
+ }
+
+ private func _getVirtualMachineInformation(parameters: M.GetVirtualMachineInformation.Request) async throws -> M.GetVirtualMachineInformation.Reply {
+ try await M.GetVirtualMachineInformation.send(parameters, to: peer)
+ }
+
+ private func _getQEMUConfiguration(parameters: M.GetQEMUConfiguration.Request) async throws -> M.GetQEMUConfiguration.Reply {
+ try await M.GetQEMUConfiguration.send(parameters, to: peer)
+ }
+
+ private func _getPackageSize(parameters: M.GetPackageSize.Request) async throws -> M.GetPackageSize.Reply {
+ try await M.GetPackageSize.send(parameters, to: peer)
+ }
+
+ private func _getPackageFile(parameters: M.GetPackageFile.Request) async throws -> M.GetPackageFile.Reply {
+ try await M.GetPackageFile.send(parameters, to: peer)
+ }
+
+ @discardableResult
+ private func _sendPackageFile(parameters: M.SendPackageFile.Request) async throws -> M.SendPackageFile.Reply {
+ try await M.SendPackageFile.send(parameters, to: peer)
+ }
+
+ @discardableResult
+ private func _deletePackageFile(parameters: M.DeletePackageFile.Request) async throws -> M.DeletePackageFile.Reply {
+ try await M.DeletePackageFile.send(parameters, to: peer)
+ }
+
+ @discardableResult
+ private func _mountGuestToolsOnVirtualMachine(parameters: M.MountGuestToolsOnVirtualMachine.Request) async throws -> M.MountGuestToolsOnVirtualMachine.Reply {
+ try await M.MountGuestToolsOnVirtualMachine.send(parameters, to: peer)
+ }
+
+ private func _startVirtualMachine(parameters: M.StartVirtualMachine.Request) async throws -> M.StartVirtualMachine.Reply {
+ try await M.StartVirtualMachine.send(parameters, to: peer)
+ }
+
+ @discardableResult
+ private func _stopVirtualMachine(parameters: M.StopVirtualMachine.Request) async throws -> M.StopVirtualMachine.Reply {
+ try await M.StopVirtualMachine.send(parameters, to: peer)
+ }
+
+ @discardableResult
+ private func _restartVirtualMachine(parameters: M.RestartVirtualMachine.Request) async throws -> M.RestartVirtualMachine.Reply {
+ try await M.RestartVirtualMachine.send(parameters, to: peer)
+ }
+
+ @discardableResult
+ private func _pauseVirtualMachine(parameters: M.PauseVirtualMachine.Request) async throws -> M.PauseVirtualMachine.Reply {
+ try await M.PauseVirtualMachine.send(parameters, to: peer)
+ }
+
+ @discardableResult
+ private func _resumeVirtualMachine(parameters: M.ResumeVirtualMachine.Request) async throws -> M.ResumeVirtualMachine.Reply {
+ try await M.ResumeVirtualMachine.send(parameters, to: peer)
+ }
+
+ @discardableResult
+ private func _saveSnapshotVirtualMachine(parameters: M.SaveSnapshotVirtualMachine.Request) async throws -> M.SaveSnapshotVirtualMachine.Reply {
+ try await M.SaveSnapshotVirtualMachine.send(parameters, to: peer)
+ }
+
+ @discardableResult
+ private func _deleteSnapshotVirtualMachine(parameters: M.DeleteSnapshotVirtualMachine.Request) async throws -> M.DeleteSnapshotVirtualMachine.Reply {
+ try await M.DeleteSnapshotVirtualMachine.send(parameters, to: peer)
+ }
+
+ @discardableResult
+ private func _restoreSnapshotVirtualMachine(parameters: M.RestoreSnapshotVirtualMachine.Request) async throws -> M.RestoreSnapshotVirtualMachine.Reply {
+ try await M.RestoreSnapshotVirtualMachine.send(parameters, to: peer)
+ }
+
+ @discardableResult
+ private func _changePointerTypeVirtualMachine(parameters: M.ChangePointerTypeVirtualMachine.Request) async throws -> M.ChangePointerTypeVirtualMachine.Reply {
+ try await M.ChangePointerTypeVirtualMachine.send(parameters, to: peer)
+ }
+ }
+}
+
+extension UTMRemoteClient {
+ enum ConnectionError: LocalizedError {
+ case cannotDetermineHost
+ case cannotFindFingerprint
+ case passwordRequired
+ case passwordInvalid
+ case fingerprintUntrusted(State.ServerFingerprint)
+ case fingerprintMismatch(State.ServerFingerprint)
+ case failedToAccessFile
+
+ var errorDescription: String? {
+ switch self {
+ case .cannotDetermineHost:
+ return NSLocalizedString("Failed to determine host name.", comment: "UTMRemoteClient")
+ case .cannotFindFingerprint:
+ return NSLocalizedString("Failed to get host fingerprint.", comment: "UTMRemoteClient")
+ case .passwordRequired:
+ return NSLocalizedString("Password is required.", comment: "UTMRemoteClient")
+ case .passwordInvalid:
+ return NSLocalizedString("Password is incorrect.", comment: "UTMRemoteClient")
+ case .fingerprintUntrusted(_):
+ return NSLocalizedString("This host is not yet trusted. You should verify that the fingerprints match what is displayed on the host and then select Trust to continue.", comment: "UTMRemoteClient")
+ case .fingerprintMismatch(_):
+ return String.localizedStringWithFormat(NSLocalizedString("The host fingerprint does not match the saved value. This means that UTM Server was reset, a different host is using the same name, or an attacker is pretending to be the host. For your protection, you need to delete this saved host to continue.", comment: "UTMRemoteClient"))
+ case .failedToAccessFile:
+ return NSLocalizedString("Failed to access file.", comment: "UTMRemoteClient")
+ }
+ }
+ }
+
+ enum ClientError: LocalizedError {
+ case versionMismatch
+
+ var errorDescription: String? {
+ switch self {
+ case .versionMismatch:
+ return NSLocalizedString("The server interface version does not match the client.", comment: "UTMRemoteClient")
+ }
+ }
+ }
+}
diff --git a/Remote/UTMRemoteConnectInterface.h b/Remote/UTMRemoteConnectInterface.h
new file mode 100644
index 000000000..814132ce5
--- /dev/null
+++ b/Remote/UTMRemoteConnectInterface.h
@@ -0,0 +1,39 @@
+//
+// Copyright © 2024 osy. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+#import
+
+@protocol UTMRemoteConnectDelegate;
+
+NS_ASSUME_NONNULL_BEGIN
+
+@protocol UTMRemoteConnectInterface
+
+@property (nonatomic, weak) id connectDelegate;
+
+- (BOOL)connectWithError:(NSError * _Nullable *)error;
+- (void)disconnect;
+
+@end
+
+@protocol UTMRemoteConnectDelegate
+
+- (void)remoteInterface:(id)remoteInterface didErrorWithMessage:(NSString *)message;
+- (void)remoteInterfaceDidConnect:(id)remoteInterface;
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/Remote/UTMRemoteKeyManager.swift b/Remote/UTMRemoteKeyManager.swift
new file mode 100644
index 000000000..4df5da5aa
--- /dev/null
+++ b/Remote/UTMRemoteKeyManager.swift
@@ -0,0 +1,196 @@
+//
+// Copyright © 2023 osy. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+import Foundation
+import Security
+import CryptoKit
+#if os(macOS)
+import SystemConfiguration
+#endif
+
+class UTMRemoteKeyManager {
+ let isClient: Bool
+ private(set) var isLoaded: Bool = false
+ private(set) var identity: SecIdentity!
+ private(set) var fingerprint: [UInt8]?
+
+ init(forClient client: Bool) {
+ self.isClient = client
+ }
+
+ private var certificateCommonNamePrefix: String {
+ "UTM Remote \(isClient ? "Client" : "Server")"
+ }
+
+ private lazy var certificateCommonName: String = {
+ #if os(macOS)
+ let deviceName = SCDynamicStoreCopyComputerName(nil, nil) as? String ?? "macOS"
+ #else
+ let deviceName = UIDevice.current.name
+ #endif
+ return "\(certificateCommonNamePrefix) (\(deviceName))"
+ }()
+
+ private func generateKey() throws -> SecIdentity {
+ let commonName = certificateCommonName as CFString
+ let organizationName = "UTM" as CFString
+ let serialNumber = Int.random(in: 1.. SecIdentity? {
+ var query = [
+ kSecClass as String: kSecClassIdentity,
+ kSecReturnRef as String: true,
+ kSecMatchLimit as String: kSecMatchLimitOne,
+ kSecMatchPolicy as String: SecPolicyCreateSSL(!isClient, nil),
+ ] as [String : Any]
+ #if os(macOS)
+ query[kSecMatchSubjectStartsWith as String] = certificateCommonNamePrefix
+ #endif
+ var copyResult: AnyObject? = nil
+ let result = SecItemCopyMatching(query as CFDictionary, ©Result)
+ if result == errSecItemNotFound {
+ return nil
+ }
+ try withSecurityThrow(result)
+ return (copyResult as! SecIdentity)
+ }
+
+ private func deleteIdentity(_ identity: SecIdentity) throws {
+ let query = [
+ kSecClass as String: kSecClassIdentity,
+ kSecMatchItemList as String: [identity],
+ ] as CFDictionary
+ try withSecurityThrow(SecItemDelete(query))
+ }
+
+ private func withSecurityThrow(_ block: @autoclosure () -> OSStatus) throws {
+ let err = block()
+ if err != errSecSuccess && err != errSecDuplicateItem {
+ throw NSError(domain: NSOSStatusErrorDomain, code: Int(err), userInfo: nil)
+ }
+ }
+}
+
+extension UTMRemoteKeyManager {
+ func load() async throws {
+ guard !isLoaded else {
+ return
+ }
+ let identity = try await Task.detached { [self] in
+ if let identity = try loadIdentity() {
+ return identity
+ } else {
+ let identity = try generateKey()
+ try importIdentity(identity)
+ return identity
+ }
+ }.value
+ var certificate: SecCertificate?
+ try withSecurityThrow(SecIdentityCopyCertificate(identity, &certificate))
+ self.identity = identity
+ self.fingerprint = certificate!.fingerprint()
+ self.isLoaded = true
+ }
+
+ func reset() async throws {
+ try await Task.detached { [self] in
+ if let identity = try loadIdentity() {
+ try deleteIdentity(identity)
+ }
+ }.value
+ if isLoaded {
+ isLoaded = false
+ try await load()
+ }
+ }
+}
+
+extension SecCertificate {
+ func fingerprint() -> [UInt8] {
+ let data = SecCertificateCopyData(self)
+ return SHA256.hash(data: data as Data).map({ $0 })
+ }
+}
+
+extension Array where Element == UInt8 {
+ func hexString() -> String {
+ self.map({ String(format: "%02X", $0) }).joined(separator: ":")
+ }
+
+ init?(hexString: String) {
+ let cleanString = hexString.replacingOccurrences(of: ":", with: "")
+ guard cleanString.count % 2 == 0 else {
+ return nil
+ }
+
+ var byteArray = [UInt8]()
+ var index = cleanString.startIndex
+
+ while index < cleanString.endIndex {
+ let nextIndex = cleanString.index(index, offsetBy: 2)
+ if let byte = UInt8(cleanString[index.. Self {
+ let length = Swift.min(lhs.count, rhs.count)
+ return (0.. Self {
+ let decoder = Decoder()
+ decoder.userInfo[.dataURL] = URL(fileURLWithPath: "/")
+ return try decoder.decode(Self.self, from: data)
+ }
+}
+
+extension Serializable where Self == UTMRemoteMessageClient.QEMUConfigurationHasChanged.Request {
+ static func decode(_ data: Data) throws -> Self {
+ let decoder = Decoder()
+ decoder.userInfo[.dataURL] = URL(fileURLWithPath: "/")
+ return try decoder.decode(Self.self, from: data)
+ }
+}
+
+extension UTMRemoteMessageClient {
+ struct ClientHandshake: Message {
+ static let id = UTMRemoteMessageClient.clientHandshake
+
+ struct Request: Serializable, Codable {
+ let version: Int
+ }
+
+ struct Reply: Serializable, Codable {
+ let version: Int
+ let capabilities: UTMCapabilities
+ }
+ }
+
+ struct ListHasChanged: Message {
+ static let id = UTMRemoteMessageClient.listHasChanged
+
+ struct Request: Serializable, Codable {
+ let ids: [UUID]
+ }
+
+ struct Reply: Serializable, Codable {}
+ }
+
+ struct QEMUConfigurationHasChanged: Message {
+ static let id = UTMRemoteMessageClient.qemuConfigurationHasChanged
+
+ struct Request: Serializable, Codable {
+ let id: UUID
+ let configuration: UTMQemuConfiguration
+ }
+
+ struct Reply: Serializable, Codable {}
+ }
+
+ struct MountedDrivesHasChanged: Message {
+ static let id = UTMRemoteMessageClient.mountedDrivesHasChanged
+
+ struct Request: Serializable, Codable {
+ let id: UUID
+ let mountedDrives: [String: String]
+ }
+
+ struct Reply: Serializable, Codable {}
+ }
+
+ struct VirtualMachineDidTransition: Message {
+ static let id = UTMRemoteMessageClient.virtualMachineDidTransition
+
+ struct Request: Serializable, Codable {
+ let id: UUID
+ let state: UTMVirtualMachineState
+ let isTakeoverAllowed: Bool
+ }
+
+ struct Reply: Serializable, Codable {}
+ }
+
+ struct VirtualMachineDidError: Message {
+ static let id = UTMRemoteMessageClient.virtualMachineDidError
+
+ struct Request: Serializable, Codable {
+ let id: UUID
+ let errorMessage: String
+ }
+
+ struct Reply: Serializable, Codable {}
+ }
+}
diff --git a/Remote/UTMRemoteServer.swift b/Remote/UTMRemoteServer.swift
new file mode 100644
index 000000000..ee39e3bee
--- /dev/null
+++ b/Remote/UTMRemoteServer.swift
@@ -0,0 +1,981 @@
+//
+// Copyright © 2023 osy. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+import Foundation
+import Combine
+import Network
+import SwiftConnect
+import SwiftPortmap
+import UserNotifications
+
+let service = "_utm_server._tcp"
+
+actor UTMRemoteServer {
+ fileprivate let data: UTMData
+ private let keyManager = UTMRemoteKeyManager(forClient: false)
+ private let center = UNUserNotificationCenter.current()
+ private let connectionQueue = DispatchQueue(label: "UTM Remote Server Connection")
+ let state: State
+
+ private var cancellables = Set()
+ private var notificationDelegate: NotificationDelegate?
+ private var listener: Task?
+ private var pendingConnections: [State.ClientFingerprint: Connection] = [:]
+ private var establishedConnections: [State.ClientFingerprint: Remote] = [:]
+ private var natPort: SwiftPortmap.Port?
+
+ private func _replaceCancellables(with set: Set) {
+ cancellables = set
+ }
+
+ @Setting("ServerAutostart") private var isServerAutostart: Bool = false
+ @Setting("ServerExternal") private var isServerExternal: Bool = false
+ @Setting("ServerAutoblock") private var isServerAutoblock: Bool = false
+ @Setting("ServerPort") private var serverPort: Int = 0
+ @Setting("ServerPasswordRequired") private var isServerPasswordRequired: Bool = false
+ @Setting("ServerPassword") private var serverPassword: String = ""
+
+ @MainActor
+ init(data: UTMData) {
+ let _state = State()
+ var _cancellables = Set()
+ self.data = data
+ self.state = _state
+
+ _cancellables.insert(_state.$approvedClients.sink { approved in
+ Task {
+ await self.approvedClientsHasChanged(approved)
+ }
+ })
+ _cancellables.insert(_state.$blockedClients.sink { blocked in
+ Task {
+ await self.blockedClientsHasChanged(blocked)
+ }
+ })
+ _cancellables.insert(_state.$connectedClients.sink { connected in
+ Task {
+ await self.connectedClientsHasChanged(connected)
+ }
+ })
+ _cancellables.insert(_state.$serverAction.sink { action in
+ guard action != .none else {
+ return
+ }
+ Task {
+ switch action {
+ case .stop:
+ await self.stop()
+ break
+ case .start:
+ await self.start()
+ break
+ case .reset:
+ await self.resetServer()
+ break
+ default:
+ break
+ }
+ self.state.requestServerAction(.none)
+ }
+ })
+ // this is a really ugly way to make sure that we keep a reference to the AnyCancellables even though
+ // we cannot access self._cancellables from init() due to it being associated with @MainActor.
+ // it should be fine because we only need to make sure the references are not dropped, we will never
+ // actually read from _cancellables
+ Task {
+ await self._replaceCancellables(with: _cancellables)
+ }
+ }
+
+ private func withErrorNotification(_ body: () async throws -> Void) async {
+ do {
+ try await body()
+ } catch {
+ if case .silentError(let error) = error as? ServerError {
+ logger.error("Error message inhibited: \(error)")
+ } else {
+ await notifyError(error)
+ }
+ }
+ }
+
+ private var metadata: NWTXTRecord {
+ NWTXTRecord(["Model": MacDevice.current.model])
+ }
+
+ func start() async {
+ do {
+ try await center.requestAuthorization(options: .alert)
+ } catch {
+ logger.error("Failed to authorize notifications.")
+ }
+ await withErrorNotification {
+ guard await !state.isServerActive else {
+ return
+ }
+ try await keyManager.load()
+ await state.setServerFingerprint(keyManager.fingerprint!)
+ registerNotifications()
+ listener = Task {
+ await withErrorNotification {
+ if isServerExternal && serverPort > 0 {
+ natPort = Port.TCP(internalPort: UInt16(serverPort))
+ natPort!.mappingChangedHandler = { port in
+ Task {
+ let address = try? await port.externalIpv4Address
+ let port = try? await port.externalPort
+ await self.state.setExternalAddress(address, port: port)
+ }
+ }
+ await withErrorNotification {
+ guard try await natPort!.externalPort == serverPort else {
+ throw ServerError.natReservationMismatch(serverPort)
+ }
+ }
+ }
+ let port = serverPort > 0 ? NWEndpoint.Port(integerLiteral: UInt16(serverPort)) : .any
+ for try await connection in Connection.advertise(on: port, forServiceType: service, txtRecord: metadata, connectionQueue: connectionQueue, identity: keyManager.identity) {
+ let connection = try? await Connection(connection: connection, connectionQueue: connectionQueue) { connection, error in
+ Task {
+ guard let fingerprint = connection.fingerprint else {
+ return
+ }
+ if !(error is NWError) {
+ // connection errors are too noisy
+ await self.notifyError(error)
+ }
+ await self.state.disconnect(fingerprint)
+ }
+ }
+ if let connection = connection {
+ await newRemoteConnection(connection)
+ }
+ }
+ }
+ natPort = nil
+ await stop()
+ }
+ await state.setServerActive(true)
+ }
+ }
+
+ func stop() async {
+ await state.disconnectAll()
+ unregisterNotifications()
+ if let listener = listener {
+ self.listener = nil
+ listener.cancel()
+ _ = await listener.result
+ }
+ await state.setExternalAddress()
+ await state.setServerActive(false)
+ }
+
+ private func newRemoteConnection(_ connection: Connection) async {
+ let remoteAddress = connection.connection.endpoint.hostname ?? "\(connection.connection.endpoint)"
+ guard let fingerprint = connection.fingerprint else {
+ connection.close()
+ return
+ }
+ guard await !state.isBlocked(fingerprint) else {
+ connection.close()
+ return
+ }
+ await state.seen(fingerprint, name: remoteAddress)
+ if await state.isApproved(fingerprint) {
+ await notifyNewConnection(remoteAddress: remoteAddress, fingerprint: fingerprint)
+ await establishConnection(connection)
+ } else if isServerAutoblock {
+ await state.block(fingerprint)
+ connection.close()
+ } else {
+ pendingConnections[fingerprint] = connection
+ await notifyNewConnection(remoteAddress: remoteAddress, fingerprint: fingerprint, isUnknown: true)
+ }
+ }
+
+ private func approvedClientsHasChanged(_ approvedClients: Set) async {
+ for approvedClient in approvedClients {
+ if let connection = pendingConnections.removeValue(forKey: approvedClient.fingerprint) {
+ await establishConnection(connection)
+ }
+ }
+ }
+
+ private func blockedClientsHasChanged(_ blockedClients: Set) {
+ for blockedClient in blockedClients {
+ if let connection = pendingConnections.removeValue(forKey: blockedClient.fingerprint) {
+ connection.close()
+ }
+ }
+ }
+
+ private func connectedClientsHasChanged(_ connectedClients: Set) {
+ for client in establishedConnections.keys {
+ if !connectedClients.contains(client) {
+ if let remote = establishedConnections.removeValue(forKey: client) {
+ remote.close()
+ Task { @MainActor in
+ await suspendSessions(for: remote)
+ }
+ }
+ }
+ }
+ }
+
+ @MainActor
+ private func suspendSessions(for remote: Remote) async {
+ let sessions = data.vmWindows.compactMap {
+ if let session = $0.value as? VMRemoteSessionState {
+ return ($0.key, session)
+ } else {
+ return nil
+ }
+ }
+ await withTaskGroup(of: Void.self) { group in
+ for (vm, session) in sessions {
+ if session.client?.id == remote.id {
+ session.client = nil
+ }
+ group.addTask {
+ try? await vm.wrapped?.pause()
+ }
+ }
+ await group.waitForAll()
+ }
+ }
+
+ private func establishConnection(_ connection: Connection) async {
+ guard let fingerprint = connection.fingerprint else {
+ connection.close()
+ return
+ }
+ await withErrorNotification {
+ let remote = Remote()
+ let local = Local(server: self, client: remote)
+ let peer = Peer(connection: connection, localInterface: local)
+ remote.peer = peer
+ do {
+ try await remote.handshake()
+ } catch {
+ if let error = error as? NWError, case .posix(let code) = error, code == .ECONNRESET {
+ // if the user canceled the connection, we don't do anything
+ throw ServerError.silentError(error)
+ }
+ peer.close()
+ throw error
+ }
+ establishedConnections.updateValue(remote, forKey: fingerprint)
+ await state.connect(fingerprint)
+ }
+ }
+
+ private func resetServer() async {
+ await withErrorNotification {
+ try await keyManager.reset()
+ await state.setServerFingerprint(keyManager.fingerprint!)
+ }
+ }
+
+ /// Send message to every connected remote client.
+ ///
+ /// If any are disconnected, we will gracefully handle the disconnect.
+ /// If `body` throws an error for any remote client (excluding NWError), then we ignore it.
+ /// - Parameter body: What to broadcast
+ func broadcast(_ body: @escaping (Remote) async throws -> Void) async {
+ enum BroadcastError: Error {
+ case connectionError(NWError, State.ClientFingerprint)
+ }
+ await withThrowingTaskGroup(of: Void.self) { group in
+ for (fingerprint, remote) in establishedConnections {
+ if Task.isCancelled {
+ break
+ }
+ group.addTask {
+ do {
+ try await body(remote)
+ } catch {
+ if let error = error as? NWError {
+ throw BroadcastError.connectionError(error, fingerprint)
+ } else {
+ throw error
+ }
+ }
+ }
+ }
+ while !group.isEmpty {
+ switch await group.nextResult() {
+ case .failure(let error):
+ if case BroadcastError.connectionError(_, let fingerprint) = error {
+ // disconnect any clients who failed to respond
+ await state.disconnect(fingerprint)
+ } else {
+ logger.error("client returned error on broadcast: \(error)")
+ }
+ default:
+ break
+ }
+ }
+ }
+ }
+}
+
+extension UTMRemoteServer {
+ private class NotificationDelegate: NSObject, UNUserNotificationCenterDelegate {
+ private let state: UTMRemoteServer.State
+
+ init(state: UTMRemoteServer.State) {
+ self.state = state
+ }
+
+ func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification) async -> UNNotificationPresentationOptions {
+ .banner
+ }
+
+ func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
+ Task {
+ let userInfo = response.notification.request.content.userInfo
+ guard let hexString = userInfo["FINGERPRINT"] as? String, let fingerprint = State.ClientFingerprint(hexString: hexString) else {
+ return
+ }
+ switch response.actionIdentifier {
+ case "ALLOW_ACTION":
+ await state.approve(fingerprint)
+ case "DENY_ACTION":
+ await state.block(fingerprint)
+ case "DISCONNECT_ACTION":
+ await state.disconnect(fingerprint)
+ default:
+ break
+ }
+ completionHandler()
+ }
+ }
+ }
+
+ private func registerNotifications() {
+ let allowAction = UNNotificationAction(identifier: "ALLOW_ACTION",
+ title: NSString.localizedUserNotificationString(forKey: "Allow", arguments: nil),
+ options: [])
+ let denyAction = UNNotificationAction(identifier: "DENY_ACTION",
+ title: NSString.localizedUserNotificationString(forKey: "Deny", arguments: nil),
+ options: [])
+ let disconnectAction = UNNotificationAction(identifier: "DISCONNECT_ACTION",
+ title: NSString.localizedUserNotificationString(forKey: "Disconnect", arguments: nil),
+ options: [])
+ let unknownRemoteCategory = UNNotificationCategory(identifier: "UNKNOWN_REMOTE_CLIENT",
+ actions: [denyAction, allowAction],
+ intentIdentifiers: [],
+ hiddenPreviewsBodyPlaceholder: NSString.localizedUserNotificationString(forKey: "New unknown remote client connection.", arguments: nil),
+ options: .customDismissAction)
+ let trustedRemoteCategory = UNNotificationCategory(identifier: "TRUSTED_REMOTE_CLIENT",
+ actions: [disconnectAction],
+ intentIdentifiers: [],
+ hiddenPreviewsBodyPlaceholder: NSString.localizedUserNotificationString(forKey: "New trusted remote client connection.", arguments: nil),
+ options: [])
+ center.setNotificationCategories([unknownRemoteCategory, trustedRemoteCategory])
+ notificationDelegate = NotificationDelegate(state: state)
+ center.delegate = notificationDelegate
+ }
+
+ private func unregisterNotifications() {
+ center.setNotificationCategories([])
+ notificationDelegate = nil
+ center.delegate = nil
+ }
+
+ private func notifyNewConnection(remoteAddress: String, fingerprint: State.ClientFingerprint, isUnknown: Bool = false) async {
+ let settings = await center.notificationSettings()
+ let combinedFingerprint = (fingerprint ^ keyManager.fingerprint!).hexString()
+ guard settings.authorizationStatus == .authorized else {
+ logger.info("Notifications disabled, ignoring connection request from '\(remoteAddress)' with fingerprint '\(combinedFingerprint)'")
+ return
+ }
+ let content = UNMutableNotificationContent()
+ if isUnknown {
+ content.title = NSString.localizedUserNotificationString(forKey: "Unknown Remote Client", arguments: nil)
+ content.body = NSString.localizedUserNotificationString(forKey: "A client with fingerprint '%@' is attempting to connect.", arguments: [combinedFingerprint])
+ content.categoryIdentifier = "UNKNOWN_REMOTE_CLIENT"
+ } else {
+ content.title = NSString.localizedUserNotificationString(forKey: "Remote Client Connected", arguments: nil)
+ content.body = NSString.localizedUserNotificationString(forKey: "Established connection from %@.", arguments: [remoteAddress])
+ content.categoryIdentifier = "TRUSTED_REMOTE_CLIENT"
+ }
+ let clientFingerprint = fingerprint.hexString()
+ content.userInfo = ["FINGERPRINT": clientFingerprint]
+ let request = UNNotificationRequest(identifier: clientFingerprint,
+ content: content,
+ trigger: nil)
+ do {
+ try await center.add(request)
+ if !isUnknown {
+ DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(15)) {
+ self.center.removeDeliveredNotifications(withIdentifiers: [clientFingerprint])
+ }
+ }
+ } catch {
+ logger.error("Error sending remote connection request: \(error.localizedDescription)")
+ }
+ }
+
+ fileprivate func notifyError(_ error: Error) async {
+ logger.error("UTM Remote Server error: '\(error)'")
+ let settings = await center.notificationSettings()
+ guard settings.authorizationStatus == .authorized else {
+ return
+ }
+ let content = UNMutableNotificationContent()
+ content.title = NSString.localizedUserNotificationString(forKey: "UTM Remote Server Error", arguments: nil)
+ content.body = error.localizedDescription
+ let request = UNNotificationRequest(identifier: UUID().uuidString,
+ content: content,
+ trigger: nil)
+ do {
+ try await center.add(request)
+ } catch {
+ logger.error("Error sending error notification: \(error.localizedDescription)")
+ }
+ }
+}
+
+extension UTMRemoteServer {
+ @MainActor
+ class State: ObservableObject {
+ typealias ClientFingerprint = [UInt8]
+ typealias ServerFingerprint = [UInt8]
+ struct Client: Codable, Identifiable, Hashable {
+ let fingerprint: ClientFingerprint
+ var name: String
+ var lastSeen: Date
+
+ var id: ClientFingerprint {
+ fingerprint
+ }
+
+ func hash(into hasher: inout Hasher) {
+ hasher.combine(fingerprint)
+ }
+
+ static func == (lhs: Client, rhs: Client) -> Bool {
+ lhs.hashValue == rhs.hashValue
+ }
+ }
+
+ enum ServerAction {
+ case none
+ case stop
+ case start
+ case reset
+ }
+
+ @Published var allClients: [Client] {
+ didSet {
+ let all = Set(allClients)
+ approvedClients.subtract(approvedClients.subtracting(all))
+ blockedClients.subtract(blockedClients.subtracting(all))
+ connectedClients.subtract(connectedClients.subtracting(all.map({ $0.fingerprint })))
+ }
+ }
+
+ @Published var approvedClients: Set {
+ didSet {
+ UserDefaults.standard.setValue(try! approvedClients.propertyList(), forKey: "TrustedClients")
+ }
+ }
+
+ @Published var blockedClients: Set {
+ didSet {
+ UserDefaults.standard.setValue(try! blockedClients.propertyList(), forKey: "BlockedClients")
+ }
+ }
+
+ @Published var connectedClients = Set()
+
+ @Published var serverAction: ServerAction = .none
+
+ var isBusy: Bool {
+ serverAction != .none
+ }
+
+ @Published private(set) var isServerActive = false
+
+ @Published private(set) var serverFingerprint: ServerFingerprint = [] {
+ didSet {
+ UserDefaults.standard.setValue(serverFingerprint.hexString(), forKey: "ServerFingerprint")
+ }
+ }
+
+ @Published private(set) var externalIPAddress: String?
+
+ @Published private(set) var externalPort: UInt16?
+
+ init() {
+ var _approvedClients = Set()
+ if let array = UserDefaults.standard.array(forKey: "TrustedClients") {
+ if let clients = try? Set(fromPropertyList: array) {
+ _approvedClients = clients
+ }
+ }
+ self.approvedClients = _approvedClients
+ var _blockedClients = Set()
+ if let array = UserDefaults.standard.array(forKey: "BlockedClients") {
+ if let clients = try? Set(fromPropertyList: array) {
+ _blockedClients = clients
+ }
+ }
+ self.blockedClients = _blockedClients
+ self.allClients = Array(_approvedClients) + Array(_blockedClients)
+ if let value = UserDefaults.standard.string(forKey: "ServerFingerprint"), let serverFingerprint = ServerFingerprint(hexString: value) {
+ self.serverFingerprint = serverFingerprint
+ }
+ }
+
+ func isConnected(_ fingerprint: ClientFingerprint) -> Bool {
+ connectedClients.contains(fingerprint)
+ }
+
+ func isApproved(_ fingerprint: ClientFingerprint) -> Bool {
+ approvedClients.contains(where: { $0.fingerprint == fingerprint }) && !isBlocked(fingerprint)
+ }
+
+ func isBlocked(_ fingerprint: ClientFingerprint) -> Bool {
+ blockedClients.contains(where: { $0.fingerprint == fingerprint })
+ }
+
+ fileprivate func setServerActive(_ isActive: Bool) {
+ isServerActive = isActive
+ }
+
+ func requestServerAction(_ action: ServerAction) {
+ serverAction = action
+ }
+
+ private func client(forFingerprint fingerprint: ClientFingerprint, name: String? = nil) -> (Int?, Client) {
+ if let idx = allClients.firstIndex(where: { $0.fingerprint == fingerprint }) {
+ if let name = name {
+ allClients[idx].name = name
+ }
+ return (idx, allClients[idx])
+ } else {
+ return (nil, Client(fingerprint: fingerprint, name: name ?? "", lastSeen: Date()))
+ }
+ }
+
+ func seen(_ fingerprint: ClientFingerprint, name: String? = nil) {
+ var (idx, client) = client(forFingerprint: fingerprint, name: name)
+ client.lastSeen = Date()
+ if let idx = idx {
+ allClients[idx] = client
+ } else {
+ allClients.append(client)
+ }
+ }
+
+ fileprivate func connect(_ fingerprint: ClientFingerprint, name: String? = nil) {
+ connectedClients.insert(fingerprint)
+ }
+
+ func disconnect(_ fingerprint: ClientFingerprint) {
+ connectedClients.remove(fingerprint)
+ }
+
+ func disconnectAll() {
+ connectedClients.removeAll()
+ }
+
+ func approve(_ fingerprint: ClientFingerprint) {
+ let (_, client) = client(forFingerprint: fingerprint)
+ approvedClients.insert(client)
+ blockedClients.remove(client)
+ }
+
+ func block(_ fingerprint: ClientFingerprint) {
+ let (_, client) = client(forFingerprint: fingerprint)
+ approvedClients.remove(client)
+ blockedClients.insert(client)
+ }
+
+ fileprivate func setServerFingerprint(_ fingerprint: ServerFingerprint) {
+ serverFingerprint = fingerprint
+ }
+
+ fileprivate func setExternalAddress(_ address: String? = nil, port: UInt16? = nil) {
+ externalIPAddress = address
+ externalPort = port
+ }
+ }
+}
+
+extension UTMRemoteServer {
+ class Local: LocalInterface {
+ typealias M = UTMRemoteMessageServer
+
+ private let server: UTMRemoteServer
+ private let client: UTMRemoteServer.Remote
+ private var isAuthenticated: Bool = false
+
+ private var data: UTMData {
+ server.data
+ }
+
+ init(server: UTMRemoteServer, client: UTMRemoteServer.Remote) {
+ self.server = server
+ self.client = client
+ }
+
+ func handle(message: M, data: Data) async throws -> Data {
+ guard isAuthenticated || message == .serverHandshake else {
+ throw ServerError.notAuthenticated
+ }
+ switch message {
+ case .serverHandshake:
+ return try await _handshake(parameters: .decode(data)).encode()
+ case .listVirtualMachines:
+ return try await _listVirtualMachines(parameters: .decode(data)).encode()
+ case .reorderVirtualMachines:
+ return try await _reorderVirtualMachines(parameters: .decode(data)).encode()
+ case .getVirtualMachineInformation:
+ return try await _getVirtualMachineInformation(parameters: .decode(data)).encode()
+ case .getQEMUConfiguration:
+ return try await _getQEMUConfiguration(parameters: .decode(data)).encode()
+ case .getPackageSize:
+ return try await _getPackageSize(parameters: .decode(data)).encode()
+ case .getPackageFile:
+ return try await _getPackageFile(parameters: .decode(data)).encode()
+ case .sendPackageFile:
+ return try await _sendPackageFile(parameters: .decode(data)).encode()
+ case .deletePackageFile:
+ return try await _deletePackageFile(parameters: .decode(data)).encode()
+ case .mountGuestToolsOnVirtualMachine:
+ return try await _mountGuestToolsOnVirtualMachine(parameters: .decode(data)).encode()
+ case .startVirtualMachine:
+ return try await _startVirtualMachine(parameters: .decode(data)).encode()
+ case .stopVirtualMachine:
+ return try await _stopVirtualMachine(parameters: .decode(data)).encode()
+ case .restartVirtualMachine:
+ return try await _restartVirtualMachine(parameters: .decode(data)).encode()
+ case .pauseVirtualMachine:
+ return try await _pauseVirtualMachine(parameters: .decode(data)).encode()
+ case .resumeVirtualMachine:
+ return try await _resumeVirtualMachine(parameters: .decode(data)).encode()
+ case .saveSnapshotVirtualMachine:
+ return try await _saveSnapshotVirtualMachine(parameters: .decode(data)).encode()
+ case .deleteSnapshotVirtualMachine:
+ return try await _deleteSnapshotVirtualMachine(parameters: .decode(data)).encode()
+ case .restoreSnapshotVirtualMachine:
+ return try await _restoreSnapshotVirtualMachine(parameters: .decode(data)).encode()
+ case .changePointerTypeVirtualMachine:
+ return try await _changePointerTypeVirtualMachine(parameters: .decode(data)).encode()
+ }
+ }
+
+ @MainActor
+ private func findVM(withId id: UUID) throws -> VMData {
+ let vm = data.virtualMachines.first(where: { $0.id == id })
+ if let vm = vm, let _ = vm.wrapped {
+ return vm
+ } else {
+ throw UTMRemoteServer.ServerError.notFound(id)
+ }
+ }
+
+ @MainActor
+ private func packageFileHasChanged(for vm: VMData, relativePathComponents: [String]) throws {
+ if relativePathComponents.count == 1 && relativePathComponents[0] == kUTMBundleScreenshotFilename {
+ try vm.wrapped?.reloadScreenshotFromFile()
+ }
+ }
+
+ private func _handshake(parameters: M.ServerHandshake.Request) async throws -> M.ServerHandshake.Reply {
+ let serverPassword = await server.serverPassword
+ if await server.isServerPasswordRequired && !serverPassword.isEmpty {
+ if serverPassword == parameters.password {
+ isAuthenticated = true
+ }
+ } else {
+ isAuthenticated = true
+ }
+ return .init(version: UTMRemoteMessageServer.version, isAuthenticated: isAuthenticated, capabilities: .current, model: MacDevice.current.model)
+ }
+
+ private func _listVirtualMachines(parameters: M.ListVirtualMachines.Request) async throws -> M.ListVirtualMachines.Reply {
+ let ids = await Task { @MainActor in
+ data.virtualMachines.map({ $0.id })
+ }.value
+ return .init(ids: ids)
+ }
+
+ private func _reorderVirtualMachines(parameters: M.ReorderVirtualMachines.Request) async throws -> M.ReorderVirtualMachines.Reply {
+ await Task { @MainActor in
+ let vms = data.virtualMachines
+ let source = parameters.ids.reduce(into: IndexSet(), { indexSet, id in
+ if let index = vms.firstIndex(where: { $0.id == id }) {
+ indexSet.insert(index)
+ }
+ })
+ let destination = min(max(0, parameters.offset), vms.count)
+ data.listMove(fromOffsets: source, toOffset: destination)
+ return .init()
+ }.value
+ }
+
+ private func _getVirtualMachineInformation(parameters: M.GetVirtualMachineInformation.Request) async throws -> M.GetVirtualMachineInformation.Reply {
+ let informations = try await Task { @MainActor in
+ try parameters.ids.map { id in
+ let vm = try findVM(withId: id)
+ let mountedDrives = vm.registryEntry?.externalDrives.mapValues({ $0.path }) ?? [:]
+ let isTakeoverAllowed = data.vmWindows[vm] is VMRemoteSessionState && (vm.state == .started || vm.state == .paused)
+ return M.VirtualMachineInformation(id: vm.id,
+ name: vm.detailsTitleLabel,
+ path: vm.pathUrl.path,
+ isShortcut: vm.isShortcut,
+ isSuspended: vm.registryEntry?.isSuspended ?? false,
+ isTakeoverAllowed: isTakeoverAllowed,
+ backend: vm.wrapped is UTMQemuVirtualMachine ? .qemu : .unknown,
+ state: vm.wrapped?.state ?? .stopped,
+ mountedDrives: mountedDrives)
+ }
+ }.value
+ return .init(informations: informations)
+ }
+
+ private func _getQEMUConfiguration(parameters: M.GetQEMUConfiguration.Request) async throws -> M.GetQEMUConfiguration.Reply {
+ let vm = try await findVM(withId: parameters.id)
+ if let config = await vm.config as? UTMQemuConfiguration {
+ return .init(configuration: config)
+ } else {
+ throw ServerError.invalidBackend
+ }
+ }
+
+ private func _getPackageSize(parameters: M.GetPackageSize.Request) async throws -> M.GetPackageSize.Reply {
+ let vm = try await findVM(withId: parameters.id)
+ let size = await data.computeSize(for: vm)
+ return .init(size: size)
+ }
+
+ private func _getPackageFile(parameters: M.GetPackageFile.Request) async throws -> M.GetPackageFile.Reply {
+ let vm = try await findVM(withId: parameters.id)
+ let fm = FileManager.default
+ let pathUrl = await vm.pathUrl
+ let fileUrl = parameters.relativePathComponents.reduce(pathUrl, { $0.appendingPathComponent($1) })
+ guard let lastModified = try fm.attributesOfItem(atPath: fileUrl.path)[.modificationDate] as? Date else {
+ throw ServerError.failedToAccessFile
+ }
+ if let requestLastModified = parameters.lastModified {
+ if lastModified.distance(to: requestLastModified).rounded(.towardZero) == 0 {
+ return .init(data: nil, lastModified: lastModified)
+ }
+ }
+ guard let data = fm.contents(atPath: fileUrl.path) else {
+ throw ServerError.failedToAccessFile
+ }
+ return .init(data: data, lastModified: lastModified)
+ }
+
+ private func _sendPackageFile(parameters: M.SendPackageFile.Request) async throws -> M.SendPackageFile.Reply {
+ let vm = try await findVM(withId: parameters.id)
+ let fm = FileManager.default
+ let pathUrl = await vm.pathUrl
+ let fileUrl = parameters.relativePathComponents.reduce(pathUrl, { $0.appendingPathComponent($1) })
+ try? fm.removeItem(at: fileUrl)
+ guard fm.createFile(atPath: fileUrl.path, contents: parameters.data, attributes: [.modificationDate: parameters.lastModified]) else {
+ throw ServerError.failedToAccessFile
+ }
+ try await packageFileHasChanged(for: vm, relativePathComponents: parameters.relativePathComponents)
+ return .init()
+ }
+
+ private func _deletePackageFile(parameters: M.DeletePackageFile.Request) async throws -> M.DeletePackageFile.Reply {
+ let vm = try await findVM(withId: parameters.id)
+ let fm = FileManager.default
+ let pathUrl = await vm.pathUrl
+ let fileUrl = parameters.relativePathComponents.reduce(pathUrl, { $0.appendingPathComponent($1) })
+ try fm.removeItem(at: fileUrl)
+ try await packageFileHasChanged(for: vm, relativePathComponents: parameters.relativePathComponents)
+ return .init()
+ }
+
+ private func _mountGuestToolsOnVirtualMachine(parameters: M.MountGuestToolsOnVirtualMachine.Request) async throws -> M.MountGuestToolsOnVirtualMachine.Reply {
+ let vm = try await findVM(withId: parameters.id)
+ if let wrapped = await vm.wrapped {
+ try await data.mountSupportTools(for: wrapped)
+ }
+ return .init()
+ }
+
+ private func _startVirtualMachine(parameters: M.StartVirtualMachine.Request) async throws -> M.StartVirtualMachine.Reply {
+ let vm = try await findVM(withId: parameters.id)
+ let serverInfo = try await data.startRemote(vm: vm, options: parameters.options, forClient: client)
+ return .init(serverInfo: serverInfo)
+ }
+
+ private func _stopVirtualMachine(parameters: M.StopVirtualMachine.Request) async throws -> M.StopVirtualMachine.Reply {
+ let vm = try await findVM(withId: parameters.id)
+ try await vm.wrapped!.stop(usingMethod: parameters.method)
+ return .init()
+ }
+
+ private func _restartVirtualMachine(parameters: M.RestartVirtualMachine.Request) async throws -> M.RestartVirtualMachine.Reply {
+ let vm = try await findVM(withId: parameters.id)
+ try await vm.wrapped!.restart()
+ return .init()
+ }
+
+ private func _pauseVirtualMachine(parameters: M.PauseVirtualMachine.Request) async throws -> M.PauseVirtualMachine.Reply {
+ let vm = try await findVM(withId: parameters.id)
+ try await vm.wrapped!.pause()
+ return .init()
+ }
+
+ private func _resumeVirtualMachine(parameters: M.ResumeVirtualMachine.Request) async throws -> M.ResumeVirtualMachine.Reply {
+ let vm = try await findVM(withId: parameters.id)
+ try await vm.wrapped!.resume()
+ return .init()
+ }
+
+ private func _saveSnapshotVirtualMachine(parameters: M.SaveSnapshotVirtualMachine.Request) async throws -> M.SaveSnapshotVirtualMachine.Reply {
+ let vm = try await findVM(withId: parameters.id)
+ try await vm.wrapped!.saveSnapshot(name: parameters.name)
+ return .init()
+ }
+
+ private func _deleteSnapshotVirtualMachine(parameters: M.DeleteSnapshotVirtualMachine.Request) async throws -> M.DeleteSnapshotVirtualMachine.Reply {
+ let vm = try await findVM(withId: parameters.id)
+ try await vm.wrapped!.deleteSnapshot(name: parameters.name)
+ return .init()
+ }
+
+ private func _restoreSnapshotVirtualMachine(parameters: M.RestoreSnapshotVirtualMachine.Request) async throws -> M.RestoreSnapshotVirtualMachine.Reply {
+ let vm = try await findVM(withId: parameters.id)
+ try await vm.wrapped!.restoreSnapshot(name: parameters.name)
+ return .init()
+ }
+
+ private func _changePointerTypeVirtualMachine(parameters: M.ChangePointerTypeVirtualMachine.Request) async throws -> M.ChangePointerTypeVirtualMachine.Reply {
+ let vm = try await findVM(withId: parameters.id)
+ guard let wrapped = await vm.wrapped as? UTMQemuVirtualMachine else {
+ throw ServerError.invalidBackend
+ }
+ try await wrapped.changeInputTablet(parameters.isTabletMode)
+ return .init()
+ }
+ }
+}
+
+extension UTMRemoteServer {
+ class Remote: Identifiable {
+ typealias M = UTMRemoteMessageClient
+ fileprivate(set) var peer: Peer!
+ let id = UUID()
+
+ func close() {
+ peer.close()
+ }
+
+ func handshake() async throws {
+ guard try await _handshake(parameters: .init(version: UTMRemoteMessageClient.version)).version == UTMRemoteMessageClient.version else {
+ throw ServerError.versionMismatch
+ }
+ }
+
+ func listHasChanged(ids: [UUID]) async throws {
+ try await _listHasChanged(parameters: .init(ids: ids))
+ }
+
+ func qemuConfigurationHasChanged(id: UUID, configuration: UTMQemuConfiguration) async throws {
+ try await _qemuConfigurationHasChanged(parameters: .init(id: id, configuration: configuration))
+ }
+
+ func mountedDrivesHasChanged(id: UUID, mountedDrives: [String: String]) async throws {
+ try await _mountedDrivesHasChanged(parameters: .init(id: id, mountedDrives: mountedDrives))
+ }
+
+ func virtualMachine(id: UUID, didTransitionToState state: UTMVirtualMachineState, isTakeoverAllowed: Bool) async throws {
+ try await _virtualMachineDidTransition(parameters: .init(id: id, state: state, isTakeoverAllowed: isTakeoverAllowed))
+ }
+
+ func virtualMachine(id: UUID, didErrorWithMessage message: String) async throws {
+ try await _virtualMachineDidError(parameters: .init(id: id, errorMessage: message))
+ }
+
+ private func _handshake(parameters: M.ClientHandshake.Request) async throws -> M.ClientHandshake.Reply {
+ try await M.ClientHandshake.send(parameters, to: peer)
+ }
+
+ @discardableResult
+ private func _listHasChanged(parameters: M.ListHasChanged.Request) async throws -> M.ListHasChanged.Reply {
+ try await M.ListHasChanged.send(parameters, to: peer)
+ }
+
+ @discardableResult
+ private func _qemuConfigurationHasChanged(parameters: M.QEMUConfigurationHasChanged.Request) async throws -> M.QEMUConfigurationHasChanged.Reply {
+ try await M.QEMUConfigurationHasChanged.send(parameters, to: peer)
+ }
+
+ @discardableResult
+ private func _mountedDrivesHasChanged(parameters: M.MountedDrivesHasChanged.Request) async throws -> M.MountedDrivesHasChanged.Reply {
+ try await M.MountedDrivesHasChanged.send(parameters, to: peer)
+ }
+
+ @discardableResult
+ private func _virtualMachineDidTransition(parameters: M.VirtualMachineDidTransition.Request) async throws -> M.VirtualMachineDidTransition.Reply {
+ try await M.VirtualMachineDidTransition.send(parameters, to: peer)
+ }
+
+ @discardableResult
+ private func _virtualMachineDidError(parameters: M.VirtualMachineDidError.Request) async throws -> M.VirtualMachineDidError.Reply {
+ try await M.VirtualMachineDidError.send(parameters, to: peer)
+ }
+ }
+}
+
+extension UTMRemoteServer {
+ enum ServerError: LocalizedError {
+ case silentError(Error)
+ case natReservationMismatch(Int)
+ case notAuthenticated
+ case versionMismatch
+ case notFound(UUID)
+ case invalidBackend
+ case failedToAccessFile
+
+ var errorDescription: String? {
+ switch self {
+ case .silentError(let error):
+ return error.localizedDescription
+ case .natReservationMismatch(let port):
+ return String.localizedStringWithFormat(NSLocalizedString("Cannot reserve port '%@' for external access from NAT. Make sure no other device on the network has reserved it.", comment: "UTMRemoteServer"), port)
+ case .notAuthenticated:
+ return NSLocalizedString("Not authenticated.", comment: "UTMRemoteServer")
+ case .versionMismatch:
+ return NSLocalizedString("The client interface version does not match the server.", comment: "UTMRemoteServer")
+ case .notFound(let id):
+ return String.localizedStringWithFormat(NSLocalizedString("Cannot find VM with ID: %@", comment: "UTMRemoteServer"), id.uuidString)
+ case .invalidBackend:
+ return NSLocalizedString("Invalid backend.", comment: "UTMRemoteServer")
+ case .failedToAccessFile:
+ return NSLocalizedString("Failed to access file.", comment: "UTMRemoteServer")
+ }
+ }
+ }
+}
+
+extension Connection {
+ var fingerprint: [UInt8]? {
+ return peerCertificateChain.first?.fingerprint()
+ }
+}
diff --git a/Remote/UTMRemoteSpiceVirtualMachine.swift b/Remote/UTMRemoteSpiceVirtualMachine.swift
new file mode 100644
index 000000000..c4736e4f9
--- /dev/null
+++ b/Remote/UTMRemoteSpiceVirtualMachine.swift
@@ -0,0 +1,424 @@
+//
+// Copyright © 2024 osy. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+import Foundation
+
+final class UTMRemoteSpiceVirtualMachine: UTMSpiceVirtualMachine {
+ struct Capabilities: UTMVirtualMachineCapabilities {
+ var supportsProcessKill: Bool {
+ true
+ }
+
+ var supportsSnapshots: Bool {
+ true
+ }
+
+ var supportsScreenshots: Bool {
+ true
+ }
+
+ var supportsDisposibleMode: Bool {
+ true
+ }
+
+ var supportsRecoveryMode: Bool {
+ false
+ }
+
+ var supportsRemoteSession: Bool {
+ false
+ }
+ }
+
+ static let capabilities = Capabilities()
+
+ private var server: UTMRemoteClient.Remote
+
+ init(packageUrl: URL, configuration: UTMQemuConfiguration, isShortcut: Bool) throws {
+ throw UTMVirtualMachineError.notImplemented
+ }
+
+ init(forRemoteServer server: UTMRemoteClient.Remote, remotePath: String, entry: UTMRegistryEntry, config: UTMQemuConfiguration) {
+ self.pathUrl = URL(fileURLWithPath: remotePath)
+ self.config = config
+ self.registryEntry = entry
+ self.server = server
+ _state = State(vm: self)
+ }
+
+ private(set) var pathUrl: URL
+
+ private(set) var isShortcut: Bool = false
+
+ private(set) var isRunningAsDisposible: Bool = false
+
+ weak var delegate: (UTMVirtualMachineDelegate)?
+
+ var onConfigurationChange: (() -> Void)?
+
+ var onStateChange: (() -> Void)?
+
+ private(set) var config: UTMQemuConfiguration {
+ willSet {
+ onConfigurationChange?()
+ }
+ }
+
+ private(set) var registryEntry: UTMRegistryEntry {
+ willSet {
+ onConfigurationChange?()
+ }
+ }
+
+ private var _state: State!
+
+ private(set) var state: UTMVirtualMachineState = .stopped {
+ willSet {
+ onStateChange?()
+ }
+
+ didSet {
+ if state == .stopped {
+ virtualMachineDidStop()
+ }
+ delegate?.virtualMachine(self, didTransitionToState: state)
+ }
+ }
+
+ var screenshot: UTMVirtualMachineScreenshot? {
+ willSet {
+ onStateChange?()
+ }
+ }
+
+ private(set) var snapshotUnsupportedError: Error?
+
+ weak var ioServiceDelegate: UTMSpiceIODelegate? {
+ didSet {
+ if let ioService = ioService {
+ ioService.delegate = ioServiceDelegate
+ }
+ }
+ }
+
+ private(set) var ioService: UTMSpiceIO? {
+ didSet {
+ oldValue?.delegate = nil
+ ioService?.delegate = ioServiceDelegate
+ }
+ }
+
+ var changeCursorRequestInProgress: Bool = false
+
+ private weak var screenshotTimer: Timer?
+
+ func reload(from packageUrl: URL?) throws {
+ throw UTMVirtualMachineError.notImplemented
+ }
+
+ @MainActor
+ func reload(usingConfiguration config: UTMQemuConfiguration) {
+ self.config = config
+ updateConfigFromRegistry()
+ }
+
+ @MainActor
+ func updateRegistry(_ entry: UTMRegistryEntry) {
+ self.registryEntry = entry
+ }
+
+ func updateConfigFromRegistry() {
+ // not needed
+ }
+
+ func changeUuid(to uuid: UUID, name: String?, copyingEntry entry: UTMRegistryEntry?) {
+ // not needed
+ }
+
+ func reconnectServer(_ body: () async throws -> UTMRemoteClient.Remote) async throws {
+ try await _state.operation(during: .resuming) {
+ self.server = try await body()
+ }
+ }
+}
+
+extension UTMRemoteSpiceVirtualMachine {
+ private class ConnectCoordinator: NSObject, UTMRemoteConnectDelegate {
+ var continuation: CheckedContinuation?
+
+ func remoteInterface(_ remoteInterface: UTMRemoteConnectInterface, didErrorWithMessage message: String) {
+ remoteInterface.connectDelegate = nil
+ continuation?.resume(throwing: VMError.spiceConnectError(message))
+ continuation = nil
+ }
+
+ func remoteInterfaceDidConnect(_ remoteInterface: UTMRemoteConnectInterface) {
+ remoteInterface.connectDelegate = nil
+ continuation?.resume()
+ continuation = nil
+ }
+ }
+}
+
+extension UTMRemoteSpiceVirtualMachine {
+ private func connect(_ serverInfo: UTMRemoteMessageServer.StartVirtualMachine.ServerInformation, options: UTMSpiceIOOptions, remoteConnection: Bool) async throws -> UTMSpiceIO {
+ let ioService = UTMSpiceIO(host: remoteConnection ? serverInfo.spiceHostExternal! : server.host,
+ tlsPort: Int(remoteConnection ? serverInfo.spicePortExternal! : serverInfo.spicePortInternal),
+ serverPublicKey: serverInfo.spicePublicKey,
+ password: serverInfo.spicePassword,
+ options: options)
+ ioService.logHandler = { (line: String) -> Void in
+ guard !line.contains("spice_make_scancode") else {
+ return // do not log key presses for privacy reasons
+ }
+ NSLog("%@", line) // FIXME: log to file
+ }
+ try ioService.start()
+ let coordinator = ConnectCoordinator()
+ try await withCheckedThrowingContinuation { continuation in
+ coordinator.continuation = continuation
+ ioService.connectDelegate = coordinator
+ do {
+ try ioService.connect()
+ } catch {
+ ioService.connectDelegate = nil
+ continuation.resume(throwing: error)
+ }
+ }
+ return ioService
+ }
+
+ func start(options: UTMVirtualMachineStartOptions) async throws {
+ try await _state.operation(before: [.stopped, .started, .paused], during: .starting, after: .started) {
+ let spiceServer = try await server.startVirtualMachine(id: id, options: options)
+ var options = UTMSpiceIOOptions()
+ if await !config.sound.isEmpty {
+ options.insert(.hasAudio)
+ }
+ if await config.sharing.hasClipboardSharing {
+ options.insert(.hasClipboardSharing)
+ }
+ if await config.sharing.isDirectoryShareReadOnly {
+ options.insert(.isShareReadOnly)
+ }
+ #if false // FIXME: verbose logging is broken on iOS
+ if hasDebugLog {
+ options.insert(.hasDebugLog)
+ }
+ #endif
+ do {
+ self.ioService = try await connect(spiceServer, options: options, remoteConnection: false)
+ } catch {
+ if spiceServer.spiceHostExternal != nil && spiceServer.spicePortExternal != nil {
+ // retry with external port
+ self.ioService = try await connect(spiceServer, options: options, remoteConnection: true)
+ } else {
+ throw error
+ }
+ }
+ if screenshotTimer == nil {
+ screenshotTimer = startScreenshotTimer()
+ }
+ }
+ }
+
+ func stop(usingMethod method: UTMVirtualMachineStopMethod) async throws {
+ try await _state.operation(before: [.started, .paused], during: .stopping, after: .stopped) {
+ await saveScreenshot()
+ try await server.stopVirtualMachine(id: id, method: method)
+ }
+ }
+
+ func restart() async throws {
+ try await _state.operation(before: [.started, .paused], during: .stopping, after: .started) {
+ try await server.restartVirtualMachine(id: id)
+ }
+ }
+
+ func pause() async throws {
+ try await _state.operation(before: .started, during: .pausing, after: .paused) {
+ try await server.pauseVirtualMachine(id: id)
+ }
+ }
+
+ func resume() async throws {
+ if ioService == nil {
+ return try await start(options: [])
+ } else {
+ try await _state.operation(before: .paused, during: .resuming, after: .started) {
+ try await server.resumeVirtualMachine(id: id)
+ }
+ }
+ }
+
+ func saveSnapshot(name: String?) async throws {
+ try await _state.operation(before: [.started, .paused], during: .saving) {
+ await saveScreenshot()
+ try await server.saveSnapshotVirtualMachine(id: id, name: name)
+ }
+ }
+
+ func deleteSnapshot(name: String?) async throws {
+ try await server.deleteSnapshotVirtualMachine(id: id, name: name)
+ }
+
+ func restoreSnapshot(name: String?) async throws {
+ try await _state.operation(before: [.started, .paused], during: .saving) {
+ try await server.restoreSnapshotVirtualMachine(id: id, name: name)
+ }
+ }
+
+ func loadScreenshotFromServer() async {
+ if let url = try? await server.getPackageFile(for: id, relativePathComponents: [kUTMBundleScreenshotFilename]) {
+ loadScreenshot(from: url)
+ }
+ }
+
+ func loadScreenshot(from url: URL) {
+ screenshot = UTMVirtualMachineScreenshot(contentsOfURL: url)
+ }
+
+ func saveScreenshot() async {
+ if let data = screenshot?.pngData {
+ try? await server.sendPackageFile(for: id, relativePathComponents: [kUTMBundleScreenshotFilename], data: data)
+ }
+ }
+
+ private func virtualMachineDidStop() {
+ ioService = nil
+ }
+}
+
+extension UTMRemoteSpiceVirtualMachine {
+ actor State {
+ private weak var vm: UTMRemoteSpiceVirtualMachine?
+ private var isInOperation: Bool = false
+ private(set) var state: UTMVirtualMachineState = .stopped {
+ didSet {
+ vm?.state = state
+ }
+ }
+ private var remoteState: UTMVirtualMachineState?
+
+ init(vm: UTMRemoteSpiceVirtualMachine) {
+ self.vm = vm
+ }
+
+ func operation(before: UTMVirtualMachineState, during: UTMVirtualMachineState, after: UTMVirtualMachineState? = nil, body: () async throws -> Void) async throws {
+ try await operation(before: [before], during: during, after: after, body: body)
+ }
+
+ func operation(before: Set? = nil, during: UTMVirtualMachineState, after: UTMVirtualMachineState? = nil, body: () async throws -> Void) async throws {
+ while isInOperation {
+ await Task.yield()
+ }
+ if let before = before {
+ guard before.contains(state) else {
+ throw VMError.operationInProgress
+ }
+ }
+ isInOperation = true
+ remoteState = nil
+ defer {
+ isInOperation = false
+ if let remoteState = remoteState {
+ state = remoteState
+ }
+ }
+ let previous = state
+ state = during
+ do {
+ try await body()
+ } catch {
+ state = previous
+ throw error
+ }
+ state = after ?? previous
+ }
+
+ func updateRemoteState(_ state: UTMVirtualMachineState) {
+ self.remoteState = state
+ if !isInOperation && self.state != state {
+ self.state = state
+ }
+ }
+ }
+
+ func updateRemoteState(_ state: UTMVirtualMachineState) async {
+ await _state.updateRemoteState(state)
+ }
+}
+
+extension UTMRemoteSpiceVirtualMachine {
+ static func isSupported(systemArchitecture: QEMUArchitecture) -> Bool {
+ true // FIXME: somehow determine which architectures are supported
+ }
+}
+
+extension UTMRemoteSpiceVirtualMachine {
+ func requestInputTablet(_ tablet: Bool) {
+ guard !changeCursorRequestInProgress else {
+ return
+ }
+ changeCursorRequestInProgress = true
+ Task {
+ defer {
+ changeCursorRequestInProgress = false
+ }
+ try await server.changePointerTypeVirtualMachine(id: id, toTabletMode: tablet)
+ ioService?.primaryInput?.requestMouseMode(!tablet)
+ }
+ }
+}
+
+extension UTMRemoteSpiceVirtualMachine {
+ func eject(_ drive: UTMQemuConfigurationDrive) async throws {
+ // FIXME: implement remote feature
+ throw UTMVirtualMachineError.notImplemented
+ }
+
+ func changeMedium(_ drive: UTMQemuConfigurationDrive, to url: URL) async throws {
+ // FIXME: implement remote feature
+ throw UTMVirtualMachineError.notImplemented
+ }
+
+}
+
+extension UTMRemoteSpiceVirtualMachine {
+ func stopAccessingPath(_ path: String) async {
+ // not needed
+ }
+
+ func changeVirtfsSharedDirectory(with bookmark: Data, isSecurityScoped: Bool) async throws {
+ throw UTMVirtualMachineError.notImplemented
+ }
+}
+
+extension UTMRemoteSpiceVirtualMachine {
+ enum VMError: LocalizedError {
+ case spiceConnectError(String)
+ case operationInProgress
+
+ var errorDescription: String? {
+ switch self {
+ case .spiceConnectError(let message):
+ return String.localizedStringWithFormat(NSLocalizedString("Failed to connect to SPICE: %@", comment: "UTMRemoteSpiceVirtualMachine"), message)
+ case .operationInProgress:
+ return NSLocalizedString("An operation is already in progress.", comment: "UTMRemoteSpiceVirtualMachine")
+ }
+ }
+ }
+}
diff --git a/Services/Swift-Bridging-Header.h b/Services/Swift-Bridging-Header.h
index 8c5774c99..24b31c470 100644
--- a/Services/Swift-Bridging-Header.h
+++ b/Services/Swift-Bridging-Header.h
@@ -25,14 +25,21 @@
#include "UTMLegacyQemuConfiguration+Sharing.h"
#include "UTMLegacyQemuConfiguration+System.h"
#include "UTMLegacyQemuConfigurationPortForward.h"
+#include "UTMLogging.h"
+#if !defined(WITH_REMOTE)
#include "UTMProcess.h"
#include "UTMQemuSystem.h"
#include "UTMJailbreak.h"
-#include "UTMLogging.h"
+#else
+#include "UTMQemuSystemBackends.h"
+#endif
#include "UTMLegacyViewState.h"
#include "UTMSpiceIO.h"
+#include "GenerateKey.h"
#if TARGET_OS_IPHONE
+#if !defined(WITH_REMOTE)
#include "UTMLocationManager.h"
+#endif
#include "VMDisplayViewController.h"
//#if !defined(TARGET_OS_VISION) || !TARGET_OS_VISION
#include "VMDisplayMetalViewController.h"
diff --git a/Services/UTMAppleVirtualMachine.swift b/Services/UTMAppleVirtualMachine.swift
index 61f2596aa..f42008659 100644
--- a/Services/UTMAppleVirtualMachine.swift
+++ b/Services/UTMAppleVirtualMachine.swift
@@ -40,6 +40,10 @@ final class UTMAppleVirtualMachine: UTMVirtualMachine {
var supportsRecoveryMode: Bool {
true
}
+
+ var supportsRemoteSession: Bool {
+ false
+ }
}
static let capabilities = Capabilities()
@@ -85,7 +89,7 @@ final class UTMAppleVirtualMachine: UTMVirtualMachine {
}
}
- private(set) var screenshot: PlatformImage? {
+ private(set) var screenshot: UTMVirtualMachineScreenshot? {
willSet {
onStateChange?()
}
@@ -474,7 +478,11 @@ final class UTMAppleVirtualMachine: UTMVirtualMachine {
screenshot = screenshotDelegate?.screenshot
return true
}
-
+
+ func reloadScreenshotFromFile() {
+ screenshot = loadScreenshot()
+ }
+
@MainActor private func createAppleVM() throws {
for i in config.serials.indices {
let (fd, sfd, name) = try createPty()
@@ -721,7 +729,7 @@ extension UTMAppleVirtualMachine: VZVirtualMachineDelegate {
}
protocol UTMScreenshotProvider: AnyObject {
- var screenshot: PlatformImage? { get }
+ var screenshot: UTMVirtualMachineScreenshot? { get }
}
enum UTMAppleVirtualMachineError: Error {
diff --git a/Services/UTMExtensions.swift b/Services/UTMExtensions.swift
index 4e464941b..bb4eabbfd 100644
--- a/Services/UTMExtensions.swift
+++ b/Services/UTMExtensions.swift
@@ -16,6 +16,7 @@
import SwiftUI
import UniformTypeIdentifiers
+import Network
extension Optional where Wrapped == String {
var _bound: String? {
@@ -383,4 +384,44 @@ extension String {
}
return Int(numeric)
}
+
+ static func random(length: Int) -> String {
+ let letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
+ return String((0.. Any {
+ let encoder = PropertyListEncoder()
+ encoder.outputFormat = .xml
+ let xml = try encoder.encode(self)
+ return try PropertyListSerialization.propertyList(from: xml, format: nil)
+ }
+}
+
+extension Decodable {
+ init(fromPropertyList propertyList: Any) throws {
+ let data = try PropertyListSerialization.data(fromPropertyList: propertyList, format: .xml, options: 0)
+ let decoder = PropertyListDecoder()
+ self = try decoder.decode(Self.self, from: data)
+ }
+}
+
+extension NWEndpoint {
+ var hostname: String? {
+ if case .hostPort(let host, _) = self {
+ switch host {
+ case .name(let hostname, _):
+ return hostname
+ case .ipv4(let address):
+ return "\(address)"
+ case .ipv6(let address):
+ return "\(address)"
+ @unknown default:
+ break
+ }
+ }
+ return nil
+ }
}
diff --git a/Services/UTMJailbreak.m b/Services/UTMJailbreak.m
index cec1d60ea..850031107 100644
--- a/Services/UTMJailbreak.m
+++ b/Services/UTMJailbreak.m
@@ -65,7 +65,7 @@
int memorystatus_control(uint32_t command, int32_t pid, uint32_t flags, user_addr_t buffer, size_t buffersize);
-#if !TARGET_OS_OSX && !defined(WITH_QEMU_TCI)
+#if !TARGET_OS_OSX && defined(WITH_JIT)
extern int csops(pid_t pid, unsigned int ops, void * useraddr, size_t usersize);
extern boolean_t exc_server(mach_msg_header_t *, mach_msg_header_t *);
extern int ptrace(int request, pid_t pid, caddr_t addr, int data);
@@ -100,7 +100,7 @@ static bool jb_has_debugger_attached(void) {
#endif
bool jb_has_cs_disabled(void) {
-#if TARGET_OS_OSX || defined(WITH_QEMU_TCI)
+#if TARGET_OS_OSX || !defined(WITH_JIT)
return false;
#else
int flags;
@@ -236,7 +236,7 @@ static bool is_device_A12_or_newer(void) {
bool jb_has_jit_entitlement(void) {
#if TARGET_OS_OSX
return true;
-#elif defined(WITH_QEMU_TCI)
+#elif !defined(WITH_JIT)
return false;
#else
NSDictionary *entitlements = cached_app_entitlements();
@@ -330,7 +330,7 @@ bool jb_has_cs_execseg_allow_unsigned(void) {
}
bool jb_enable_ptrace_hack(void) {
-#if TARGET_OS_OSX || defined(WITH_QEMU_TCI)
+#if TARGET_OS_OSX || !defined(WITH_JIT)
return false;
#else
bool debugged = jb_has_debugger_attached();
@@ -380,7 +380,7 @@ bool jb_increase_memlimit(void) {
return ret1 == 0 && ret2 == 0;
}
-#if !TARGET_OS_OSX && !defined(WITH_QEMU_TCI)
+#if !TARGET_OS_OSX && defined(WITH_JIT)
extern const char *environ[];
static char *childArgv[] = {NULL, "debugme", NULL};
@@ -397,7 +397,7 @@ bool jb_spawn_ptrace_child(int argc, char **argv) {
return false;
}
childArgv[0] = argv[0];
- if ((ret = posix_spawnp(&pid, argv[0], NULL, NULL, (void *)childArgv, (void *)environ)) != 0) {
+ if ((ret = posix_spawnp(&pid, argv[0], NULL, NULL, (void *)childArgv, NULL)) != 0) {
return false;
}
return true;
diff --git a/Services/UTMLogging.m b/Services/UTMLogging.m
index 2d7812c51..7e15efa4c 100644
--- a/Services/UTMLogging.m
+++ b/Services/UTMLogging.m
@@ -15,7 +15,9 @@
//
#import "UTMLogging.h"
+#if !defined(WITH_REMOTE)
@import QEMUKitInternal;
+#endif
static UTMLogging *gLoggingInstance;
@@ -42,7 +44,11 @@ + (UTMLogging *)sharedInstance {
}
- (void)writeLine:(NSString *)line {
+#if defined(WITH_REMOTE)
+ NSLog(@"%@", line);
+#else
[QEMULogging.sharedInstance writeLine:line];
+#endif
}
@end
diff --git a/Services/UTMPasteboard.swift b/Services/UTMPasteboard.swift
index bea8de9df..7fb5a8be4 100644
--- a/Services/UTMPasteboard.swift
+++ b/Services/UTMPasteboard.swift
@@ -26,7 +26,7 @@ typealias SystemPasteboardType = NSPasteboard.PasteboardType
#else
#error("Neither UIKit nor AppKit found!")
#endif
-#if WITH_QEMU_TCI
+#if !WITH_USB
import CocoaSpiceNoUsb
#else
import CocoaSpice
diff --git a/Services/UTMPipeInterface.swift b/Services/UTMPipeInterface.swift
new file mode 100644
index 000000000..8f1fc5c0e
--- /dev/null
+++ b/Services/UTMPipeInterface.swift
@@ -0,0 +1,152 @@
+//
+// Copyright © 2024 osy. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+import Foundation
+import QEMUKit
+
+class UTMPipeInterface: NSObject, QEMUInterface {
+ weak var connectDelegate: QEMUInterfaceConnectDelegate?
+
+ var monitorOutPipeURL: URL!
+ var monitorInPipeURL: URL!
+ var guestAgentOutPipeURL: URL!
+ var guestAgentInPipeURL: URL!
+
+ private var pipeIOQueue = DispatchQueue(label: "UTMPipeInterface")
+ private var qemuMonitorPort: Port!
+ private var qemuGuestAgentPort: Port!
+
+ func start() throws {
+ try initializePipe(at: monitorOutPipeURL)
+ try initializePipe(at: monitorInPipeURL)
+ try initializePipe(at: guestAgentOutPipeURL)
+ try initializePipe(at: guestAgentInPipeURL)
+ }
+
+ func connect() throws {
+ pipeIOQueue.async { [self] in
+ do {
+ try openQemuPipes()
+ connectDelegate?.qemuInterface(self, didCreateMonitorPort: qemuMonitorPort)
+ connectDelegate?.qemuInterface(self, didCreateGuestAgentPort: qemuGuestAgentPort)
+ } catch {
+ connectDelegate?.qemuInterface(self, didErrorWithMessage: error.localizedDescription)
+ }
+ }
+ }
+
+ func disconnect() {
+ cleanupPipes()
+ }
+}
+
+extension UTMPipeInterface {
+ class Port: NSObject, QEMUPort {
+ let readPipe: FileHandle
+
+ let writePipe: FileHandle
+
+ var readDataHandler: readDataHandler_t?
+
+ var errorHandler: errorHandler_t?
+
+ var disconnectHandler: disconnectHandler_t?
+
+ let isOpen: Bool = true
+
+ init(readPipe: FileHandle, writePipe: FileHandle) {
+ self.readPipe = readPipe
+ self.writePipe = writePipe
+ super.init()
+ readPipe.readabilityHandler = { fileHandle in
+ self.readDataHandler?(fileHandle.availableData)
+ }
+ }
+
+ func write(_ data: Data) {
+ writePipe.write(data)
+ }
+ }
+
+ private var fileManager: FileManager {
+ FileManager.default
+ }
+
+ private func initializePipe(at url: URL) throws {
+ if fileManager.fileExists(atPath: url.path) {
+ try fileManager.removeItem(at: url)
+ }
+ guard mkfifo(url.path, S_IRUSR | S_IWUSR) == 0 else {
+ throw ServerError.failedToCreatePipe(errno)
+ }
+ }
+
+ private func openPipe(at url: URL, forReading isRead: Bool) throws -> FileHandle {
+ let fileHandle: FileHandle
+ if isRead {
+ fileHandle = try FileHandle(forReadingFrom: url)
+ } else {
+ fileHandle = try FileHandle(forWritingTo: url)
+ }
+ return fileHandle
+ }
+
+ private func cleanupPipes() {
+ // unblock any un-opened pipes
+ _ = try? FileHandle(forUpdating: monitorOutPipeURL)
+ _ = try? FileHandle(forUpdating: monitorInPipeURL)
+ _ = try? FileHandle(forUpdating: guestAgentOutPipeURL)
+ _ = try? FileHandle(forUpdating: guestAgentInPipeURL)
+ pipeIOQueue.sync {
+ if let monitorOutPipeURL = monitorOutPipeURL {
+ try? fileManager.removeItem(at: monitorOutPipeURL)
+ }
+ if let monitorInPipeURL = monitorInPipeURL {
+ try? fileManager.removeItem(at: monitorInPipeURL)
+ }
+ if let guestAgentOutPipeURL = guestAgentOutPipeURL {
+ try? fileManager.removeItem(at: guestAgentOutPipeURL)
+ }
+ if let guestAgentInPipeURL = guestAgentInPipeURL {
+ try? fileManager.removeItem(at: guestAgentInPipeURL)
+ }
+ qemuMonitorPort = nil
+ qemuGuestAgentPort = nil
+ }
+ }
+
+ private func openQemuPipes() throws {
+ let qmpReadPipe = try openPipe(at: monitorOutPipeURL, forReading: true)
+ let qmpWritePipe = try openPipe(at: monitorInPipeURL, forReading: false)
+ qemuMonitorPort = Port(readPipe: qmpReadPipe, writePipe: qmpWritePipe)
+ let qgaReadPipe = try openPipe(at: guestAgentOutPipeURL, forReading: true)
+ let qgaWritePipe = try openPipe(at: guestAgentInPipeURL, forReading: false)
+ qemuGuestAgentPort = Port(readPipe: qgaReadPipe, writePipe: qgaWritePipe)
+ }
+}
+
+extension UTMPipeInterface {
+ enum ServerError: LocalizedError {
+ case failedToCreatePipe(Int32)
+
+ var errorDescription: String? {
+ switch self {
+ case .failedToCreatePipe(_):
+ return NSLocalizedString("Failed to create pipe for communications.", comment: "UTMPipeInterface")
+ }
+ }
+ }
+}
diff --git a/Services/UTMQemuPort.swift b/Services/UTMQemuPort.swift
index f9df727ea..62023bbf7 100644
--- a/Services/UTMQemuPort.swift
+++ b/Services/UTMQemuPort.swift
@@ -15,7 +15,7 @@
//
import QEMUKitInternal
-#if WITH_QEMU_TCI
+#if !WITH_USB
import CocoaSpiceNoUsb
#else
import CocoaSpice
diff --git a/Services/UTMQemuSystem.h b/Services/UTMQemuSystem.h
index f1fbfa3ed..218ef1721 100644
--- a/Services/UTMQemuSystem.h
+++ b/Services/UTMQemuSystem.h
@@ -15,24 +15,9 @@
//
#import "UTMProcess.h"
+#import "UTMQemuSystemBackends.h"
@import QEMUKitInternal;
-/// Specify the backend renderer for this VM
-typedef NS_ENUM(NSInteger, UTMQEMURendererBackend) {
- kQEMURendererBackendDefault = 0,
- kQEMURendererBackendAngleGL = 1,
- kQEMURendererBackendAngleMetal = 2,
- kQEMURendererBackendMax = 3,
-};
-
-/// Specify the sound backend for this VM
-typedef NS_ENUM(NSInteger, UTMQEMUSoundBackend) {
- kQEMUSoundBackendDefault = 0,
- kQEMUSoundBackendSPICE = 1,
- kQEMUSoundBackendCoreAudio = 2,
- kQEMUSoundBackendMax = 3,
-};
-
NS_ASSUME_NONNULL_BEGIN
@interface UTMQemuSystem : UTMProcess
diff --git a/Services/UTMQemuSystemBackends.h b/Services/UTMQemuSystemBackends.h
new file mode 100644
index 000000000..f8eb12d7e
--- /dev/null
+++ b/Services/UTMQemuSystemBackends.h
@@ -0,0 +1,36 @@
+//
+// Copyright © 2024 osy. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+#ifndef UTMQemuSystemBackends_h
+#define UTMQemuSystemBackends_h
+
+/// Specify the backend renderer for this VM
+typedef NS_ENUM(NSInteger, UTMQEMURendererBackend) {
+ kQEMURendererBackendDefault = 0,
+ kQEMURendererBackendAngleGL = 1,
+ kQEMURendererBackendAngleMetal = 2,
+ kQEMURendererBackendMax = 3,
+};
+
+/// Specify the sound backend for this VM
+typedef NS_ENUM(NSInteger, UTMQEMUSoundBackend) {
+ kQEMUSoundBackendDefault = 0,
+ kQEMUSoundBackendSPICE = 1,
+ kQEMUSoundBackendCoreAudio = 2,
+ kQEMUSoundBackendMax = 3,
+};
+
+#endif /* UTMQemuSystemBackends_h */
diff --git a/Services/UTMQemuVirtualMachine.swift b/Services/UTMQemuVirtualMachine.swift
index c162e6a76..122acac91 100644
--- a/Services/UTMQemuVirtualMachine.swift
+++ b/Services/UTMQemuVirtualMachine.swift
@@ -16,13 +16,16 @@
import Foundation
import QEMUKit
+#if os(macOS)
+import SwiftPortmap
+#endif
private var SpiceIoServiceGuestAgentContext = 0
private let kSuspendSnapshotName = "suspend"
private let kProbeSuspendDelay = 1*NSEC_PER_SEC
/// QEMU backend virtual machine
-final class UTMQemuVirtualMachine: UTMVirtualMachine {
+final class UTMQemuVirtualMachine: UTMSpiceVirtualMachine {
struct Capabilities: UTMVirtualMachineCapabilities {
var supportsProcessKill: Bool {
true
@@ -43,6 +46,10 @@ final class UTMQemuVirtualMachine: UTMVirtualMachine {
var supportsRecoveryMode: Bool {
false
}
+
+ var supportsRemoteSession: Bool {
+ true
+ }
}
static let capabilities = Capabilities()
@@ -88,7 +95,7 @@ final class UTMQemuVirtualMachine: UTMVirtualMachine {
}
}
- private(set) var screenshot: PlatformImage? {
+ var screenshot: UTMVirtualMachineScreenshot? {
willSet {
onStateChange?()
}
@@ -117,6 +124,9 @@ final class UTMQemuVirtualMachine: UTMVirtualMachine {
}
}
+ /// Pipe interface (alternative to UTMSpiceIO)
+ private var pipeInterface: UTMPipeInterface?
+
private let qemuVM = QEMUVirtualMachine()
private var system: UTMQemuSystem? {
@@ -144,7 +154,13 @@ final class UTMQemuVirtualMachine: UTMVirtualMachine {
private var swtpm: UTMSWTPM?
private var changeCursorRequestInProgress: Bool = false
-
+
+ #if WITH_SERVER
+ @Setting("ServerPort") private var serverPort: Int = 0
+ private var spicePort: SwiftPortmap.Port?
+ private(set) var spiceServerInfo: UTMRemoteMessageServer.StartVirtualMachine.ServerInformation?
+ #endif
+
@MainActor required init(packageUrl: URL, configuration: UTMQemuConfiguration, isShortcut: Bool = false) throws {
self.isScopedAccess = packageUrl.startAccessingSecurityScopedResource()
// load configuration
@@ -267,10 +283,24 @@ extension UTMQemuVirtualMachine {
await qemuVM.setRedirectLog(url: nil)
}
let isRunningAsDisposible = options.contains(.bootDisposibleMode)
+ let isRemoteSession = options.contains(.remoteSession)
+ #if WITH_SERVER
+ let spicePassword = isRemoteSession ? String.random(length: 32) : nil
+ let spicePort = isRemoteSession ? try SwiftPortmap.Port.TCP(unusedPortStartingAt: UInt16(serverPort)) : nil
+ #else
+ if isRemoteSession {
+ throw UTMVirtualMachineError.notImplemented
+ }
+ #endif
await MainActor.run {
config.qemu.isDisposable = isRunningAsDisposible
+ #if WITH_SERVER
+ config.qemu.spiceServerPort = spicePort?.internalPort
+ config.qemu.spiceServerPassword = spicePassword
+ config.qemu.isSpiceServerTlsEnabled = true
+ #endif
}
-
+
// start TPM
if await config.qemu.hasTPMDevice {
let swtpm = UTMSWTPM()
@@ -280,12 +310,12 @@ extension UTMQemuVirtualMachine {
try await swtpm.start()
self.swtpm = swtpm
}
-
+
let allArguments = await config.allArguments
let arguments = allArguments.map({ $0.string })
let resources = allArguments.compactMap({ $0.fileUrls }).flatMap({ $0 })
let remoteBookmarks = await remoteBookmarks
-
+
let system = await UTMQemuSystem(arguments: arguments, architecture: config.system.architecture.rawValue)
system.resources = resources
system.currentDirectoryUrl = await config.socketURL
@@ -295,12 +325,12 @@ extension UTMQemuVirtualMachine {
system.hasDebugLog = hasDebugLog
#endif
try Task.checkCancellation()
-
+
if isShortcut {
try await accessShortcut()
try Task.checkCancellation()
}
-
+
var options = UTMSpiceIOOptions()
if await !config.sound.isEmpty {
options.insert(.hasAudio)
@@ -317,14 +347,41 @@ extension UTMQemuVirtualMachine {
}
#endif
let spiceSocketUrl = await config.spiceSocketURL
- let ioService = UTMSpiceIO(socketUrl: spiceSocketUrl, options: options)
- ioService.logHandler = { [weak system] (line: String) -> Void in
- guard !line.contains("spice_make_scancode") else {
- return // do not log key presses for privacy reasons
+ let interface: any QEMUInterface
+ let spicePublicKey: Data?
+ if isRemoteSession {
+ let pipeInterface = UTMPipeInterface()
+ await MainActor.run {
+ pipeInterface.monitorInPipeURL = config.monitorPipeURL.appendingPathExtension("in")
+ pipeInterface.monitorOutPipeURL = config.monitorPipeURL.appendingPathExtension("out")
+ pipeInterface.guestAgentInPipeURL = config.guestAgentPipeURL.appendingPathExtension("in")
+ pipeInterface.guestAgentOutPipeURL = config.guestAgentPipeURL.appendingPathExtension("out")
}
- system?.logging?.writeLine(line)
+ try pipeInterface.start()
+ interface = pipeInterface
+ // generate a TLS key for this session
+ guard let key = GenerateRSACertificate("UTM Remote SPICE Server" as CFString,
+ "UTM" as CFString,
+ Int.random(in: 1.. Void in
+ guard !line.contains("spice_make_scancode") else {
+ return // do not log key presses for privacy reasons
+ }
+ system?.logging?.writeLine(line)
+ }
+ try ioService.start()
+ interface = ioService
+ spicePublicKey = nil
}
- try ioService.start()
try Task.checkCancellation()
// create EFI variables for legacy config as well as handle UEFI resets
@@ -333,7 +390,7 @@ extension UTMQemuVirtualMachine {
// start QEMU
await qemuVM.setDelegate(self)
- try await qemuVM.start(launcher: system, interface: ioService)
+ try await qemuVM.start(launcher: system, interface: interface)
let monitor = await monitor!
try Task.checkCancellation()
@@ -346,7 +403,11 @@ extension UTMQemuVirtualMachine {
// set up SPICE sharing and removable drives
try await self.restoreExternalDrives(withMounting: !isSuspended)
- try await self.restoreSharedDirectory(for: ioService)
+ if let ioService = interface as? UTMSpiceIO {
+ try await self.restoreSharedDirectory(for: ioService)
+ } else {
+ // TODO: implement shared directory in remote interface
+ }
try Task.checkCancellation()
// continue VM boot
@@ -358,11 +419,24 @@ extension UTMQemuVirtualMachine {
}
// save ioService and let it set the delegate
- self.ioService = ioService
+ self.ioService = interface as? UTMSpiceIO
+ self.pipeInterface = interface as? UTMPipeInterface
self.isRunningAsDisposible = isRunningAsDisposible
// test out snapshots
self.snapshotUnsupportedError = await determineSnapshotSupport()
+
+ #if WITH_SERVER
+ // save server details
+ if let spicePort = spicePort, let spicePublicKey = spicePublicKey, let spicePassword = spicePassword {
+ self.spiceServerInfo = .init(spicePortInternal: spicePort.internalPort,
+ spicePortExternal: try? await spicePort.externalPort,
+ spiceHostExternal: try? await spicePort.externalIpv4Address,
+ spicePublicKey: spicePublicKey,
+ spicePassword: spicePassword)
+ self.spicePort = spicePort
+ }
+ #endif
}
func start(options: UTMVirtualMachineStartOptions = []) async throws {
@@ -379,7 +453,7 @@ extension UTMQemuVirtualMachine {
}
try await startTask!.value
state = .started
- if screenshotTimer == nil {
+ if screenshotTimer == nil && !options.contains(.remoteSession) {
screenshotTimer = startScreenshotTimer()
}
} catch {
@@ -584,10 +658,16 @@ extension UTMQemuVirtualMachine: QEMUVirtualMachineDelegate {
}
func qemuVMDidStop(_ qemuVM: QEMUVirtualMachine) {
+ #if WITH_SERVER
+ spicePort = nil
+ spiceServerInfo = nil
+ #endif
swtpm?.stop()
swtpm = nil
ioService = nil
ioServiceDelegate = nil
+ pipeInterface?.disconnect()
+ pipeInterface = nil
snapshotUnsupportedError = nil
try? saveScreenshot()
state = .stopped
@@ -621,11 +701,27 @@ extension UTMQemuVirtualMachine: QEMUVirtualMachineDelegate {
// MARK: - Input device switching
extension UTMQemuVirtualMachine {
- func requestInputTablet(_ tablet: Bool) {
- guard !changeCursorRequestInProgress else {
+ func changeInputTablet(_ tablet: Bool) async throws {
+ defer {
+ changeCursorRequestInProgress = false
+ }
+ guard state == .started else {
+ return
+ }
+ guard let monitor = await monitor else {
return
}
- guard let spiceIO = ioService else {
+ do {
+ let index = try await monitor.mouseIndex(forAbsolute: tablet)
+ try await monitor.mouseSelect(index)
+ ioService?.primaryInput?.requestMouseMode(!tablet)
+ } catch {
+ logger.error("Error changing mouse mode: \(error)")
+ }
+ }
+
+ func requestInputTablet(_ tablet: Bool) {
+ guard !changeCursorRequestInProgress else {
return
}
changeCursorRequestInProgress = true
@@ -633,40 +729,11 @@ extension UTMQemuVirtualMachine {
defer {
changeCursorRequestInProgress = false
}
- guard state == .started else {
- return
- }
- guard let monitor = await monitor else {
- return
- }
- do {
- let index = try await monitor.mouseIndex(forAbsolute: tablet)
- try await monitor.mouseSelect(index)
- spiceIO.primaryInput?.requestMouseMode(!tablet)
- } catch {
- logger.error("Error changing mouse mode: \(error)")
- }
+ try await changeInputTablet(tablet)
}
}
}
-// MARK: - USB redirection
-extension UTMQemuVirtualMachine {
- var hasUsbRedirection: Bool {
- return jb_has_usb_entitlement()
- }
-}
-
-// MARK: - Screenshot
-extension UTMQemuVirtualMachine {
- @MainActor @discardableResult
- func takeScreenshot() async -> Bool {
- let screenshot = await ioService?.screenshot()
- self.screenshot = screenshot?.image
- return true
- }
-}
-
// MARK: - Architecture supported
extension UTMQemuVirtualMachine {
/// Check if a QEMU target is supported
@@ -695,7 +762,11 @@ extension UTMQemuVirtualMachine {
// MARK: - External drives
extension UTMQemuVirtualMachine {
- func eject(_ drive: UTMQemuConfigurationDrive, isForced: Bool = false) async throws {
+ func eject(_ drive: UTMQemuConfigurationDrive) async throws {
+ try await eject(drive, isForced: false)
+ }
+
+ private func eject(_ drive: UTMQemuConfigurationDrive, isForced: Bool) async throws {
guard drive.isExternal else {
return
}
@@ -707,8 +778,12 @@ extension UTMQemuVirtualMachine {
}
await registryEntry.removeExternalDrive(forId: drive.id)
}
-
- func changeMedium(_ drive: UTMQemuConfigurationDrive, to url: URL, isAccessOnly: Bool = false) async throws {
+
+ func changeMedium(_ drive: UTMQemuConfigurationDrive, to url: URL) async throws {
+ try await changeMedium(drive, to: url, isAccessOnly: false)
+ }
+
+ private func changeMedium(_ drive: UTMQemuConfigurationDrive, to url: URL, isAccessOnly: Bool) async throws {
_ = url.startAccessingSecurityScopedResource()
defer {
url.stopAccessingSecurityScopedResource()
@@ -719,7 +794,7 @@ extension UTMQemuVirtualMachine {
await registryEntry.setExternalDrive(file, forId: drive.id)
try await changeMedium(drive, with: tempBookmark, url: url, isSecurityScoped: false, isAccessOnly: isAccessOnly)
}
-
+
private func changeMedium(_ drive: UTMQemuConfigurationDrive, with bookmark: Data, url: URL?, isSecurityScoped: Bool, isAccessOnly: Bool) async throws {
let system = await system ?? UTMProcess()
let (success, bookmark, path) = await system.accessData(withBookmark: bookmark, securityScoped: isSecurityScoped)
@@ -731,8 +806,8 @@ extension UTMQemuVirtualMachine {
try qemu.changeMedium(forDrive: "drive\(drive.id)", path: path)
}
}
-
- func restoreExternalDrives(withMounting isMounting: Bool) async throws {
+
+ private func restoreExternalDrives(withMounting isMounting: Bool) async throws {
guard await system != nil else {
throw UTMQemuVirtualMachineError.invalidVmState
}
@@ -754,43 +829,14 @@ extension UTMQemuVirtualMachine {
}
}
}
-
- @MainActor func externalImageURL(for drive: UTMQemuConfigurationDrive) -> URL? {
- registryEntry.externalDrives[drive.id]?.url
- }
}
// MARK: - Shared directory
extension UTMQemuVirtualMachine {
- @MainActor var sharedDirectoryURL: URL? {
- registryEntry.sharedDirectories.first?.url
- }
-
- func clearSharedDirectory() async {
- if let oldPath = await registryEntry.sharedDirectories.first?.path {
- await system?.stopAccessingPath(oldPath)
- }
- await registryEntry.removeAllSharedDirectories()
- }
-
- func changeSharedDirectory(to url: URL) async throws {
- await clearSharedDirectory()
- _ = url.startAccessingSecurityScopedResource()
- defer {
- url.stopAccessingSecurityScopedResource()
- }
- let file = try await UTMRegistryEntry.File(url: url, isReadOnly: config.sharing.isDirectoryShareReadOnly)
- await registryEntry.setSingleSharedDirectory(file)
- if await config.sharing.directoryShareMode == .webdav {
- if let ioService = ioService {
- ioService.changeSharedDirectory(url)
- }
- } else if await config.sharing.directoryShareMode == .virtfs {
- let tempBookmark = try url.bookmarkData()
- try await changeVirtfsSharedDirectory(with: tempBookmark, isSecurityScoped: false)
- }
+ func stopAccessingPath(_ path: String) async {
+ await system?.stopAccessingPath(path)
}
-
+
func changeVirtfsSharedDirectory(with bookmark: Data, isSecurityScoped: Bool) async throws {
let system = await system ?? UTMProcess()
let (success, bookmark, path) = await system.accessData(withBookmark: bookmark, securityScoped: isSecurityScoped)
@@ -799,61 +845,10 @@ extension UTMQemuVirtualMachine {
}
await registryEntry.updateSingleSharedDirectoryRemoteBookmark(bookmark)
}
-
- func restoreSharedDirectory(for ioService: UTMSpiceIO) async throws {
- guard let share = await registryEntry.sharedDirectories.first else {
- return
- }
- if await config.sharing.directoryShareMode == .virtfs {
- if let bookmark = share.remoteBookmark {
- // a share bookmark was saved while QEMU was running
- try await changeVirtfsSharedDirectory(with: bookmark, isSecurityScoped: true)
- } else {
- // a share bookmark was saved while QEMU was NOT running
- let url = try URL(resolvingPersistentBookmarkData: share.bookmark)
- try await changeSharedDirectory(to: url)
- }
- } else if await config.sharing.directoryShareMode == .webdav {
- ioService.changeSharedDirectory(share.url)
- }
- }
}
// MARK: - Registry syncing
extension UTMQemuVirtualMachine {
- @MainActor func updateRegistryFromConfig() async throws {
- // save a copy to not collide with updateConfigFromRegistry()
- let configShare = config.sharing.directoryShareUrl
- let configDrives = config.drives
- try await updateRegistryBasics()
- for drive in configDrives {
- if drive.isExternal, let url = drive.imageURL {
- try await changeMedium(drive, to: url)
- } else if drive.isExternal {
- try await eject(drive)
- }
- }
- if let url = configShare {
- try await changeSharedDirectory(to: url)
- } else {
- await clearSharedDirectory()
- }
- // remove any unreferenced drives
- registryEntry.externalDrives = registryEntry.externalDrives.filter({ element in
- configDrives.contains(where: { $0.id == element.key && $0.isExternal })
- })
- }
-
- @MainActor func updateConfigFromRegistry() {
- config.sharing.directoryShareUrl = sharedDirectoryURL
- for i in config.drives.indices {
- let id = config.drives[i].id
- if config.drives[i].isExternal {
- config.drives[i].imageURL = registryEntry.externalDrives[id]?.url
- }
- }
- }
-
@MainActor func changeUuid(to uuid: UUID, name: String? = nil, copyingEntry entry: UTMRegistryEntry? = nil) {
config.information.uuid = uuid
if let name = name {
@@ -864,7 +859,7 @@ extension UTMQemuVirtualMachine {
registryEntry.update(copying: entry)
}
}
-
+
@MainActor var remoteBookmarks: [URL: Data] {
var dict = [URL: Data]()
for file in registryEntry.externalDrives.values {
@@ -889,6 +884,7 @@ enum UTMQemuVirtualMachineError: Error {
case accessShareFailed
case invalidVmState
case saveSnapshotFailed(Error)
+ case keyGenerationFailed
}
extension UTMQemuVirtualMachineError: LocalizedError {
@@ -905,6 +901,8 @@ extension UTMQemuVirtualMachineError: LocalizedError {
case .invalidVmState: return NSLocalizedString("The virtual machine is in an invalid state.", comment: "UTMQemuVirtualMachine")
case .saveSnapshotFailed(let error):
return String.localizedStringWithFormat(NSLocalizedString("Failed to save VM snapshot. Usually this means at least one device does not support snapshots. %@", comment: "UTMQemuVirtualMachine"), error.localizedDescription)
+ case .keyGenerationFailed:
+ return NSLocalizedString("Failed to generate TLS key for server.", comment: "UTMQemuVirtualMachine")
}
}
}
diff --git a/Services/UTMRegistry.swift b/Services/UTMRegistry.swift
index 7007b388a..1b5e69517 100644
--- a/Services/UTMRegistry.swift
+++ b/Services/UTMRegistry.swift
@@ -59,7 +59,7 @@ class UTMRegistry: NSObject {
super.init()
if let newEntries = try? serializedEntries.mapValues({ value in
let dict = value as! [String: Any]
- return try UTMRegistryEntry(from: dict)
+ return try UTMRegistryEntry(fromPropertyList: dict)
}) {
entries = newEntries
}
diff --git a/Services/UTMRegistryEntry.swift b/Services/UTMRegistryEntry.swift
index 16a81c186..fa812b909 100644
--- a/Services/UTMRegistryEntry.swift
+++ b/Services/UTMRegistryEntry.swift
@@ -15,6 +15,7 @@
//
import Foundation
+import Combine
@objc class UTMRegistryEntry: NSObject, Codable, ObservableObject {
/// Empty registry entry used only as a workaround for object initialization
@@ -61,7 +62,7 @@ import Foundation
} else {
package = nil
}
- _package = package ?? File(path: path)
+ _package = package ?? File(dummyFromPath: path)
self.uuid = uuid
_isSuspended = false
_externalDrives = [:]
@@ -109,11 +110,7 @@ import Foundation
}
func asDictionary() throws -> [String: Any] {
- let encoder = PropertyListEncoder()
- encoder.outputFormat = .xml
- let xml = try encoder.encode(self)
- let dict = try PropertyListSerialization.propertyList(from: xml, format: nil)
- return dict as! [String: Any]
+ return try propertyList() as! [String: Any]
}
/// Update the UUID
@@ -128,13 +125,6 @@ import Foundation
protocol UTMRegistryEntryDecodable: Decodable {}
extension UTMRegistryEntry: UTMRegistryEntryDecodable {}
-extension UTMRegistryEntryDecodable {
- init(from dictionary: [String: Any]) throws {
- let data = try PropertyListSerialization.data(fromPropertyList: dictionary, format: .xml, options: 0)
- let decoder = PropertyListDecoder()
- self = try decoder.decode(Self.self, from: data)
- }
-}
// MARK: - Accessors
@MainActor extension UTMRegistryEntry {
@@ -177,7 +167,11 @@ extension UTMRegistryEntryDecodable {
_externalDrives = newValue
}
}
-
+
+ var externalDrivePublisher: Published<[String: File]>.Publisher {
+ $_externalDrives
+ }
+
var sharedDirectories: [File] {
get {
_sharedDirectories
@@ -308,7 +302,7 @@ extension UTMRegistryEntry {
}
for drive in viewState.allDrives() {
if let bookmark = viewState.bookmark(forRemovableDrive: drive), let path = viewState.path(forRemovableDrive: drive) {
- let file = File(path: path, remoteBookmark: bookmark)
+ let file = File(dummyFromPath: path, remoteBookmark: bookmark)
_externalDrives[drive] = file
}
}
@@ -393,7 +387,7 @@ extension UTMRegistryEntry {
self.isValid = true
}
- fileprivate init(path: String, remoteBookmark: Data = Data()) {
+ init(dummyFromPath path: String, remoteBookmark: Data = Data()) {
self.path = path
self.bookmark = Data()
self.isReadOnly = false
diff --git a/Services/UTMSpiceIO.h b/Services/UTMSpiceIO.h
index 2b3b87646..1e0f9dac8 100644
--- a/Services/UTMSpiceIO.h
+++ b/Services/UTMSpiceIO.h
@@ -16,8 +16,12 @@
#import
#import "UTMSpiceIODelegate.h"
+#if defined(WITH_REMOTE)
+#import "UTMRemoteConnectInterface.h"
+#else
@import QEMUKitInternal;
-#if defined(WITH_QEMU_TCI)
+#endif
+#if !defined(WITH_USB)
@import CocoaSpiceNoUsb;
#else
@import CocoaSpice;
@@ -34,14 +38,18 @@ typedef NS_OPTIONS(NSUInteger, UTMSpiceIOOptions) {
NS_ASSUME_NONNULL_BEGIN
+#if defined(WITH_REMOTE)
+@interface UTMSpiceIO : NSObject
+#else
@interface UTMSpiceIO : NSObject
+#endif
@property (nonatomic, readonly, nullable) CSDisplay *primaryDisplay;
@property (nonatomic, readonly, nullable) CSInput *primaryInput;
@property (nonatomic, readonly, nullable) CSPort *primarySerial;
@property (nonatomic, readonly) NSArray *displays;
@property (nonatomic, readonly) NSArray *serials;
-#if !defined(WITH_QEMU_TCI)
+#if defined(WITH_USB)
@property (nonatomic, readonly, nullable) CSUSBManager *primaryUsbManager;
#endif
@property (nonatomic, weak, nullable) id delegate;
@@ -50,6 +58,7 @@ NS_ASSUME_NONNULL_BEGIN
- (instancetype)init NS_UNAVAILABLE;
- (instancetype)initWithSocketUrl:(NSURL *)socketUrl options:(UTMSpiceIOOptions)options NS_DESIGNATED_INITIALIZER;
+- (instancetype)initWithHost:(NSString *)host tlsPort:(NSInteger)tlsPort serverPublicKey:(NSData *)serverPublicKey password:(NSString *)password options:(UTMSpiceIOOptions)options NS_DESIGNATED_INITIALIZER;
- (void)changeSharedDirectory:(NSURL *)url;
- (BOOL)startWithError:(NSError * _Nullable *)error;
diff --git a/Services/UTMSpiceIO.m b/Services/UTMSpiceIO.m
index 857c397bd..2128ad737 100644
--- a/Services/UTMSpiceIO.m
+++ b/Services/UTMSpiceIO.m
@@ -22,20 +22,23 @@
@interface UTMSpiceIO ()
-@property (nonatomic) NSURL *socketUrl;
+@property (nonatomic, nullable) NSURL *socketUrl;
+@property (nonatomic, nullable) NSString *host;
+@property (nonatomic) NSInteger tlsPort;
+@property (nonatomic, nullable) NSData *serverPublicKey;
+@property (nonatomic, nullable) NSString *password;
@property (nonatomic) UTMSpiceIOOptions options;
@property (nonatomic, readwrite, nullable) CSDisplay *primaryDisplay;
@property (nonatomic) NSMutableArray *mutableDisplays;
@property (nonatomic, readwrite, nullable) CSInput *primaryInput;
@property (nonatomic, readwrite, nullable) CSPort *primarySerial;
@property (nonatomic) NSMutableArray *mutableSerials;
-#if !defined(WITH_QEMU_TCI)
+#if defined(WITH_USB)
@property (nonatomic, readwrite, nullable) CSUSBManager *primaryUsbManager;
#endif
@property (nonatomic, nullable) CSConnection *spiceConnection;
@property (nonatomic, nullable) CSMain *spice;
@property (nonatomic, nullable, copy) NSURL *sharedDirectory;
-@property (nonatomic) NSInteger port;
@property (nonatomic) BOOL dynamicResolutionSupported;
@property (nonatomic, readwrite) BOOL isConnected;
@@ -72,10 +75,29 @@ - (instancetype)initWithSocketUrl:(NSURL *)socketUrl options:(UTMSpiceIOOptions)
return self;
}
+- (instancetype)initWithHost:(NSString *)host tlsPort:(NSInteger)tlsPort serverPublicKey:(NSData *)serverPublicKey password:(NSString *)password options:(UTMSpiceIOOptions)options {
+ if (self = [super init]) {
+ self.host = host;
+ self.tlsPort = tlsPort;
+ self.serverPublicKey = serverPublicKey;
+ self.password = password;
+ self.options = options;
+ self.mutableDisplays = [NSMutableArray array];
+ self.mutableSerials = [NSMutableArray array];
+ }
+
+ return self;
+}
+
- (void)initializeSpiceIfNeeded {
if (!self.spiceConnection) {
- NSURL *relativeSocketFile = [NSURL fileURLWithPath:self.socketUrl.lastPathComponent];
- self.spiceConnection = [[CSConnection alloc] initWithUnixSocketFile:relativeSocketFile];
+ if (self.socketUrl) {
+ NSURL *relativeSocketFile = [NSURL fileURLWithPath:self.socketUrl.lastPathComponent];
+ self.spiceConnection = [[CSConnection alloc] initWithUnixSocketFile:relativeSocketFile];
+ } else {
+ self.spiceConnection = [[CSConnection alloc] initWithHost:self.host tlsPort:[@(self.tlsPort) stringValue] serverPublicKey:self.serverPublicKey];
+ self.spiceConnection.password = self.password;
+ }
self.spiceConnection.delegate = self;
self.spiceConnection.audioEnabled = (self.options & UTMSpiceIOOptionsHasAudio) == UTMSpiceIOOptionsHasAudio;
self.spiceConnection.session.shareClipboard = (self.options & UTMSpiceIOOptionsHasClipboardSharing) == UTMSpiceIOOptionsHasClipboardSharing;
@@ -94,13 +116,15 @@ - (BOOL)startWithError:(NSError * _Nullable *)error {
}
// do not need to encode/decode audio locally
g_setenv("SPICE_DISABLE_OPUS", "1", YES);
- // need to chdir to workaround AF_UNIX sun_len limitations
- NSString *curdir = self.socketUrl.URLByDeletingLastPathComponent.path;
- if (!curdir || ![NSFileManager.defaultManager changeCurrentDirectoryPath:curdir]) {
- if (error) {
- *error = [NSError errorWithDomain:kUTMErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey: NSLocalizedString(@"Failed to change current directory.", "UTMSpiceIO")}];
+ if (self.socketUrl) {
+ // need to chdir to workaround AF_UNIX sun_len limitations
+ NSString *curdir = self.socketUrl.URLByDeletingLastPathComponent.path;
+ if (!curdir || ![NSFileManager.defaultManager changeCurrentDirectoryPath:curdir]) {
+ if (error) {
+ *error = [NSError errorWithDomain:kUTMErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey: NSLocalizedString(@"Failed to change current directory.", "UTMSpiceIO")}];
+ }
+ return NO;
}
- return NO;
}
if (![self.spice spiceStart]) {
if (error) {
@@ -135,7 +159,7 @@ - (void)disconnect {
self.primaryInput = nil;
self.primarySerial = nil;
[self.mutableSerials removeAllObjects];
-#if !defined(WITH_QEMU_TCI)
+#if defined(WITH_USB)
self.primaryUsbManager = nil;
#endif
}
@@ -154,10 +178,13 @@ - (void)screenshotWithCompletion:(screenshotCallback_t)completion {
- (void)spiceConnected:(CSConnection *)connection {
NSAssert(connection == self.spiceConnection, @"Unknown connection");
self.isConnected = YES;
-#if !defined(WITH_QEMU_TCI)
+#if defined(WITH_USB)
self.primaryUsbManager = connection.usbManager;
[self.delegate spiceDidChangeUsbManager:connection.usbManager];
#endif
+#if defined(WITH_REMOTE)
+ [self.connectDelegate remoteInterfaceDidConnect:self];
+#endif
}
- (void)spiceInputAvailable:(CSConnection *)connection input:(CSInput *)input {
@@ -177,12 +204,19 @@ - (void)spiceInputUnavailable:(CSConnection *)connection input:(CSInput *)input
- (void)spiceDisconnected:(CSConnection *)connection {
NSAssert(connection == self.spiceConnection, @"Unknown connection");
self.isConnected = NO;
+ if ([self.delegate respondsToSelector:@selector(spiceDidDisconnect)]) {
+ [self.delegate spiceDidDisconnect];
+ }
}
- (void)spiceError:(CSConnection *)connection code:(CSConnectionError)code message:(nullable NSString *)message {
NSAssert(connection == self.spiceConnection, @"Unknown connection");
self.isConnected = NO;
+#if defined(WITH_REMOTE)
+ [self.connectDelegate remoteInterface:self didErrorWithMessage:message];
+#else
[self.connectDelegate qemuInterface:self didErrorWithMessage:message];
+#endif
}
- (void)spiceDisplayCreated:(CSConnection *)connection display:(CSDisplay *)display {
@@ -202,6 +236,9 @@ - (void)spiceDisplayUpdated:(CSConnection *)connection display:(CSDisplay *)disp
- (void)spiceDisplayDestroyed:(CSConnection *)connection display:(CSDisplay *)display {
NSAssert(connection == self.spiceConnection, @"Unknown connection");
[self.mutableDisplays removeObject:display];
+ if (self.primaryDisplay == display) {
+ self.primaryDisplay = nil;
+ }
[self.delegate spiceDidDestroyDisplay:display];
}
@@ -215,12 +252,16 @@ - (void)spiceAgentDisconnected:(CSConnection *)connection {
- (void)spiceForwardedPortOpened:(CSConnection *)connection port:(CSPort *)port {
if ([port.name isEqualToString:@"org.qemu.monitor.qmp.0"]) {
+#if !defined(WITH_REMOTE)
UTMQemuPort *qemuPort = [[UTMQemuPort alloc] initFrom:port];
[self.connectDelegate qemuInterface:self didCreateMonitorPort:qemuPort];
+#endif
}
if ([port.name isEqualToString:@"org.qemu.guest_agent.0"]) {
+#if !defined(WITH_REMOTE)
UTMQemuPort *qemuPort = [[UTMQemuPort alloc] initFrom:port];
[self.connectDelegate qemuInterface:self didCreateGuestAgentPort:qemuPort];
+#endif
}
if ([port.name isEqualToString:@"com.utmapp.terminal.0"]) {
self.primarySerial = port;
@@ -236,11 +277,11 @@ - (void)spiceForwardedPortClosed:(CSConnection *)connection port:(CSPort *)port
}
if ([port.name isEqualToString:@"org.qemu.guest_agent.0"]) {
}
- if ([port.name isEqualToString:@"com.utmapp.terminal.0"]) {
- self.primarySerial = port;
- }
if ([port.name hasPrefix:@"com.utmapp.terminal."]) {
[self.mutableSerials removeObject:port];
+ if (self.primarySerial == port) {
+ self.primarySerial = nil;
+ }
[self.delegate spiceDidDestroySerial:port];
}
}
@@ -285,7 +326,7 @@ - (void)setDelegate:(id)delegate {
if (self.primarySerial) {
[self.delegate spiceDidCreateSerial:self.primarySerial];
}
-#if !defined(WITH_QEMU_TCI)
+#if defined(WITH_USB)
if (self.primaryUsbManager) {
[self.delegate spiceDidChangeUsbManager:self.primaryUsbManager];
}
diff --git a/Services/UTMSpiceIODelegate.h b/Services/UTMSpiceIODelegate.h
index a6376cda6..e38ff7bb7 100644
--- a/Services/UTMSpiceIODelegate.h
+++ b/Services/UTMSpiceIODelegate.h
@@ -32,12 +32,13 @@ NS_ASSUME_NONNULL_BEGIN
- (void)spiceDidUpdateDisplay:(CSDisplay *)display NS_SWIFT_NAME(spiceDidUpdateDisplay(_:));
- (void)spiceDidCreateSerial:(CSPort *)serial NS_SWIFT_NAME(spiceDidCreateSerial(_:));
- (void)spiceDidDestroySerial:(CSPort *)serial NS_SWIFT_NAME(spiceDidDestroySerial(_:));
-#if !defined(WITH_QEMU_TCI)
+#if defined(WITH_USB)
- (void)spiceDidChangeUsbManager:(nullable CSUSBManager *)usbManager NS_SWIFT_NAME(spiceDidChangeUsbManager(_:));
#endif
@optional
- (void)spiceDynamicResolutionSupportDidChange:(BOOL)supported;
+- (void)spiceDidDisconnect;
@end
diff --git a/Services/UTMSpiceVirtualMachine.swift b/Services/UTMSpiceVirtualMachine.swift
new file mode 100644
index 000000000..6adbf24a7
--- /dev/null
+++ b/Services/UTMSpiceVirtualMachine.swift
@@ -0,0 +1,177 @@
+//
+// Copyright © 2024 osy. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+import Foundation
+
+/// Common methods for all SPICE virtual machines
+protocol UTMSpiceVirtualMachine: UTMVirtualMachine where Configuration == UTMQemuConfiguration {
+ /// Set when VM is running with saving changes
+ var isRunningAsDisposible: Bool { get }
+
+ /// Get and set screenshot
+ var screenshot: UTMVirtualMachineScreenshot? { get set }
+
+ /// Handles IO
+ var ioServiceDelegate: UTMSpiceIODelegate? { get set }
+
+ /// SPICE interface
+ var ioService: UTMSpiceIO? { get }
+
+ /// Change input mode
+ /// - Parameter tablet: If true, mouse events will be absolute
+ func requestInputTablet(_ tablet: Bool)
+
+ /// Eject a removable drive
+ /// - Parameter drive: Removable drive
+ func eject(_ drive: UTMQemuConfigurationDrive) async throws
+
+ /// Change mount image of a removable drive
+ /// - Parameters:
+ /// - drive: Removable drive
+ /// - url: New mount image
+ func changeMedium(_ drive: UTMQemuConfigurationDrive, to url: URL) async throws
+
+ /// Release resources for accessing a path
+ /// - Parameter path: Path to stop accessing
+ func stopAccessingPath(_ path: String) async
+
+ /// Setup access to a VirtFS shared directory
+ ///
+ /// Throw an exception if this is not supported.
+ /// - Parameters:
+ /// - bookmark: Bookmark to access
+ /// - isSecurityScoped: Is the bookmark security scoped?
+ func changeVirtfsSharedDirectory(with bookmark: Data, isSecurityScoped: Bool) async throws
+}
+
+// MARK: - USB redirection
+extension UTMSpiceVirtualMachine {
+ var hasUsbRedirection: Bool {
+ #if HAS_USB
+ return jb_has_usb_entitlement()
+ #else
+ return false
+ #endif
+ }
+}
+
+// MARK: - Screenshot
+extension UTMSpiceVirtualMachine {
+ @MainActor @discardableResult
+ func takeScreenshot() async -> Bool {
+ if let screenshot = await ioService?.screenshot() {
+ self.screenshot = UTMVirtualMachineScreenshot(wrapping: screenshot.image)
+ }
+ return true
+ }
+
+ func reloadScreenshotFromFile() {
+ screenshot = loadScreenshot()
+ }
+}
+
+// MARK: - External drives
+extension UTMSpiceVirtualMachine {
+ @MainActor func externalImageURL(for drive: UTMQemuConfigurationDrive) -> URL? {
+ registryEntry.externalDrives[drive.id]?.url
+ }
+}
+
+// MARK: - Shared directory
+extension UTMSpiceVirtualMachine {
+ @MainActor var sharedDirectoryURL: URL? {
+ registryEntry.sharedDirectories.first?.url
+ }
+
+ func clearSharedDirectory() async {
+ if let oldPath = await registryEntry.sharedDirectories.first?.path {
+ await stopAccessingPath(oldPath)
+ }
+ await registryEntry.removeAllSharedDirectories()
+ }
+
+ func changeSharedDirectory(to url: URL) async throws {
+ await clearSharedDirectory()
+ _ = url.startAccessingSecurityScopedResource()
+ defer {
+ url.stopAccessingSecurityScopedResource()
+ }
+ let file = try await UTMRegistryEntry.File(url: url, isReadOnly: config.sharing.isDirectoryShareReadOnly)
+ await registryEntry.setSingleSharedDirectory(file)
+ if await config.sharing.directoryShareMode == .webdav {
+ if let ioService = ioService {
+ ioService.changeSharedDirectory(url)
+ }
+ } else if await config.sharing.directoryShareMode == .virtfs {
+ let tempBookmark = try url.bookmarkData()
+ try await changeVirtfsSharedDirectory(with: tempBookmark, isSecurityScoped: false)
+ }
+ }
+
+ func restoreSharedDirectory(for ioService: UTMSpiceIO) async throws {
+ guard let share = await registryEntry.sharedDirectories.first else {
+ return
+ }
+ if await config.sharing.directoryShareMode == .virtfs {
+ if let bookmark = share.remoteBookmark {
+ // a share bookmark was saved while QEMU was running
+ try await changeVirtfsSharedDirectory(with: bookmark, isSecurityScoped: true)
+ } else {
+ // a share bookmark was saved while QEMU was NOT running
+ let url = try URL(resolvingPersistentBookmarkData: share.bookmark)
+ try await changeSharedDirectory(to: url)
+ }
+ } else if await config.sharing.directoryShareMode == .webdav {
+ ioService.changeSharedDirectory(share.url)
+ }
+ }
+}
+
+// MARK: - Registry syncing
+extension UTMSpiceVirtualMachine {
+ @MainActor func updateRegistryFromConfig() async throws {
+ // save a copy to not collide with updateConfigFromRegistry()
+ let configShare = config.sharing.directoryShareUrl
+ let configDrives = config.drives
+ try await updateRegistryBasics()
+ for drive in configDrives {
+ if drive.isExternal, let url = drive.imageURL {
+ try await changeMedium(drive, to: url)
+ } else if drive.isExternal {
+ try await eject(drive)
+ }
+ }
+ if let url = configShare {
+ try await changeSharedDirectory(to: url)
+ } else {
+ await clearSharedDirectory()
+ }
+ // remove any unreferenced drives
+ registryEntry.externalDrives = registryEntry.externalDrives.filter({ element in
+ configDrives.contains(where: { $0.id == element.key && $0.isExternal })
+ })
+ }
+
+ @MainActor func updateConfigFromRegistry() {
+ config.sharing.directoryShareUrl = sharedDirectoryURL
+ for i in config.drives.indices {
+ let id = config.drives[i].id
+ if config.drives[i].isExternal {
+ config.drives[i].imageURL = registryEntry.externalDrives[id]?.url
+ }
+ }
+ }
+}
diff --git a/Services/UTMVirtualMachine.swift b/Services/UTMVirtualMachine.swift
index c5e8d0f03..b7eec04e1 100644
--- a/Services/UTMVirtualMachine.swift
+++ b/Services/UTMVirtualMachine.swift
@@ -24,7 +24,7 @@ import UIKit
private let kUTMBundleExtension = "utm"
private let kScreenshotPeriodSeconds = 60.0
-private let kUTMBundleScreenshotFilename = "screenshot.png"
+let kUTMBundleScreenshotFilename = "screenshot.png"
private let kUTMBundleViewFilename = "view.plist"
/// UTM virtual machine backend
@@ -66,8 +66,8 @@ protocol UTMVirtualMachine: AnyObject, Identifiable {
var state: UTMVirtualMachineState { get }
/// If non-null, is the most recent screenshot of the running VM
- var screenshot: PlatformImage? { get }
-
+ var screenshot: UTMVirtualMachineScreenshot? { get }
+
/// If non-null, `saveSnapshot` and `restoreSnapshot` will not work due to the reason specified
var snapshotUnsupportedError: Error? { get }
@@ -149,6 +149,9 @@ protocol UTMVirtualMachine: AnyObject, Identifiable {
/// Request a screenshot of the primary graphics device
/// - Returns: true if successful and the screenshot will be in `screenshot`
@discardableResult func takeScreenshot() async -> Bool
+
+ /// If screenshot is modified externally, this must be called
+ func reloadScreenshotFromFile() throws
}
/// Supported capabilities for a UTM backend
@@ -167,6 +170,9 @@ protocol UTMVirtualMachineCapabilities {
/// The backend supports booting into recoveryOS.
var supportsRecoveryMode: Bool { get }
+
+ /// The backend supports remote sessions.
+ var supportsRemoteSession: Bool { get }
}
/// Delegate for UTMVirtualMachine events
@@ -201,7 +207,7 @@ protocol UTMVirtualMachineDelegate: AnyObject {
}
/// Virtual machine state
-enum UTMVirtualMachineState {
+enum UTMVirtualMachineState: Codable {
case stopped
case starting
case started
@@ -214,17 +220,19 @@ enum UTMVirtualMachineState {
}
/// Additional options for VM start
-struct UTMVirtualMachineStartOptions: OptionSet {
+struct UTMVirtualMachineStartOptions: OptionSet, Codable {
let rawValue: UInt
/// Boot without persisting any changes.
static let bootDisposibleMode = Self(rawValue: 1 << 0)
/// Boot into recoveryOS (when supported).
static let bootRecovery = Self(rawValue: 1 << 1)
+ /// Start VDI session where a remote client will connect to.
+ static let remoteSession = Self(rawValue: 1 << 2)
}
/// Method to stop the VM
-enum UTMVirtualMachineStopMethod {
+enum UTMVirtualMachineStopMethod: Codable {
/// Sends a request to the guest to shut down gracefully.
case request
/// Sends a hardware power down signal.
@@ -282,6 +290,43 @@ extension UTMVirtualMachine {
// MARK: - Screenshot
+struct UTMVirtualMachineScreenshot {
+ let image: PlatformImage
+ let pngData: Data?
+
+ init?(contentsOfURL url: URL) {
+ #if canImport(AppKit)
+ guard let image = NSImage(contentsOf: url) else {
+ return nil
+ }
+ #elseif canImport(UIKit)
+ guard let image = UIImage(contentsOfURL: url) else {
+ return nil
+ }
+ #endif
+ self.image = image
+ self.pngData = Self.createData(from: image)
+ }
+
+ init(wrapping image: PlatformImage) {
+ self.image = image
+ self.pngData = Self.createData(from: image)
+ }
+
+ private static func createData(from image: PlatformImage) -> Data? {
+ #if canImport(AppKit)
+ guard let cgref = image.cgImage(forProposedRect: nil, context: nil, hints: nil) else {
+ return nil
+ }
+ let newrep = NSBitmapImageRep(cgImage: cgref)
+ newrep.size = image.size
+ return newrep.representation(using: .png, properties: [:])
+ #elseif canImport(UIKit)
+ return image.pngData()
+ #endif
+ }
+}
+
extension UTMVirtualMachine {
private var isScreenshotSaveEnabled: Bool {
!UserDefaults.standard.bool(forKey: "NoSaveScreenshot")
@@ -311,12 +356,8 @@ extension UTMVirtualMachine {
return timer
}
- func loadScreenshot() -> PlatformImage? {
- #if canImport(AppKit)
- return NSImage(contentsOf: screenshotUrl)
- #elseif canImport(UIKit)
- return UIImage(contentsOfURL: screenshotUrl)
- #endif
+ func loadScreenshot() -> UTMVirtualMachineScreenshot? {
+ UTMVirtualMachineScreenshot(contentsOfURL: screenshotUrl)
}
func saveScreenshot() throws {
@@ -326,17 +367,7 @@ extension UTMVirtualMachine {
guard let screenshot = screenshot else {
return
}
- #if canImport(AppKit)
- guard let cgref = screenshot.cgImage(forProposedRect: nil, context: nil, hints: nil) else {
- return
- }
- let newrep = NSBitmapImageRep(cgImage: cgref)
- newrep.size = screenshot.size
- let pngdata = newrep.representation(using: .png, properties: [:])
- try pngdata?.write(to: screenshotUrl)
- #elseif canImport(UIKit)
- try screenshot.pngData()?.write(to: screenshotUrl)
- #endif
+ try screenshot.pngData?.write(to: screenshotUrl)
}
func deleteScreenshot() throws {
diff --git a/UTM.xcodeproj/project.pbxproj b/UTM.xcodeproj/project.pbxproj
index 68cf5cd0e..b28e0edf5 100644
--- a/UTM.xcodeproj/project.pbxproj
+++ b/UTM.xcodeproj/project.pbxproj
@@ -320,6 +320,7 @@
CE064C662A563F4B003C833D /* swtpm.0.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE064C642A563F4A003C833D /* swtpm.0.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
CE064C6A2A563F6E003C833D /* swtpm.0.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE064C642A563F4A003C833D /* swtpm.0.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
CE064C6C2A563F75003C833D /* swtpm.0.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE064C642A563F4A003C833D /* swtpm.0.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+ CE08334B2B784FD400522C03 /* RemoteContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE08334A2B784FD400522C03 /* RemoteContentView.swift */; };
CE0B6CEC24AD532500FE012D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = CE550BD52259479D0063E575 /* Assets.xcassets */; };
CE0B6CED24AD532A00FE012D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = CE550BD52259479D0063E575 /* Assets.xcassets */; };
CE0B6CF324AD568400FE012D /* UTMLegacyQemuConfiguration.m in Sources */ = {isa = PBXBuildFile; fileRef = CE31C244225E555600A965DD /* UTMLegacyQemuConfiguration.m */; };
@@ -423,6 +424,8 @@
CE19392626DCB094005CEC17 /* RAMSlider.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE19392526DCB093005CEC17 /* RAMSlider.swift */; };
CE19392726DCB094005CEC17 /* RAMSlider.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE19392526DCB093005CEC17 /* RAMSlider.swift */; };
CE19392826DCB094005CEC17 /* RAMSlider.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE19392526DCB093005CEC17 /* RAMSlider.swift */; };
+ CE1AEC3F2B78B30700992AFC /* MacDeviceLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE1AEC3E2B78B30700992AFC /* MacDeviceLabel.swift */; };
+ CE1AEC402B78B30700992AFC /* MacDeviceLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE1AEC3E2B78B30700992AFC /* MacDeviceLabel.swift */; };
CE25124729BFDB87000790AB /* UTMScriptingGuestProcessImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE25124629BFDB87000790AB /* UTMScriptingGuestProcessImpl.swift */; };
CE25124929BFDBA6000790AB /* UTMScriptingGuestFileImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE25124829BFDBA6000790AB /* UTMScriptingGuestFileImpl.swift */; };
CE25124B29BFE273000790AB /* UTMScriptable.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE25124A29BFE273000790AB /* UTMScriptable.swift */; };
@@ -610,6 +613,7 @@
CE2D958E24AD4F990059923A /* UTMApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE2D955524AD4F980059923A /* UTMApp.swift */; };
CE2D958F24AD4FF00059923A /* VMCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE2D954324AD4F980059923A /* VMCardView.swift */; };
CE2D959024AD50D50059923A /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 521F3EFB2414F73800130500 /* Localizable.strings */; };
+ CE38EC692B5DB3AE008B324B /* UTMRemoteClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE38EC682B5DB3AE008B324B /* UTMRemoteClient.swift */; };
CE4698F924C8FBD9008C1BD6 /* Icons in Resources */ = {isa = PBXBuildFile; fileRef = CE4698F824C8FBD9008C1BD6 /* Icons */; };
CE4698FA24C8FBD9008C1BD6 /* Icons in Resources */ = {isa = PBXBuildFile; fileRef = CE4698F824C8FBD9008C1BD6 /* Icons */; };
CE5076DB250AB55D00C26C19 /* VMDisplayMetalViewController+Pencil.m in Sources */ = {isa = PBXBuildFile; fileRef = CE5076DA250AB55D00C26C19 /* VMDisplayMetalViewController+Pencil.m */; platformFilter = ios; };
@@ -627,8 +631,11 @@
CE612AC624D3B50700FA6300 /* VMDisplayWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE612AC524D3B50700FA6300 /* VMDisplayWindowController.swift */; };
CE65BABF26A4D8DD0001BD6B /* VMConfigDisplayConsoleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8401FDA5269D44E400265F0D /* VMConfigDisplayConsoleView.swift */; };
CE65BAC026A4D8DE0001BD6B /* VMConfigDisplayConsoleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8401FDA5269D44E400265F0D /* VMConfigDisplayConsoleView.swift */; };
+ CE6C13CA2B63610C003B7032 /* UTMRemoteMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE6C13C92B63610C003B7032 /* UTMRemoteMessage.swift */; };
+ CE6C13CB2B63610C003B7032 /* UTMRemoteMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE6C13C92B63610C003B7032 /* UTMRemoteMessage.swift */; };
CE6D21DC2553A6ED001D29C5 /* VMConfirmActionModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE6D21DB2553A6ED001D29C5 /* VMConfirmActionModifier.swift */; };
CE6D21DD2553A6ED001D29C5 /* VMConfirmActionModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE6D21DB2553A6ED001D29C5 /* VMConfirmActionModifier.swift */; };
+ CE70E8D52B648FBE007FA787 /* UTMRemoteSpiceVirtualMachine.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE70E8D42B648FBE007FA787 /* UTMRemoteSpiceVirtualMachine.swift */; };
CE772AAC25C8B0F600E4E379 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE772AAB25C8B0F600E4E379 /* ContentView.swift */; };
CE772AAD25C8B0F600E4E379 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE772AAB25C8B0F600E4E379 /* ContentView.swift */; };
CE772AB325C8B7B500E4E379 /* VMCommands.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE772AB225C8B7B500E4E379 /* VMCommands.swift */; };
@@ -639,6 +646,9 @@
CE8813D324CD230300532628 /* ActivityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE8813D224CD230300532628 /* ActivityView.swift */; };
CE8813D524CD265700532628 /* VMShareFileModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE8813D424CD265700532628 /* VMShareFileModifier.swift */; };
CE8813D624CD265700532628 /* VMShareFileModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE8813D424CD265700532628 /* VMShareFileModifier.swift */; };
+ CE89CB0E2B8B1B5A006B2CC2 /* VisionKeyboardKit in Frameworks */ = {isa = PBXBuildFile; platformFilters = (xros, ); productRef = CE89CB0D2B8B1B5A006B2CC2 /* VisionKeyboardKit */; };
+ CE89CB102B8B1B6A006B2CC2 /* VisionKeyboardKit in Frameworks */ = {isa = PBXBuildFile; platformFilters = (xros, ); productRef = CE89CB0F2B8B1B6A006B2CC2 /* VisionKeyboardKit */; };
+ CE89CB122B8B1B7A006B2CC2 /* VisionKeyboardKit in Frameworks */ = {isa = PBXBuildFile; platformFilters = (xros, ); productRef = CE89CB112B8B1B7A006B2CC2 /* VisionKeyboardKit */; };
CE928C2A26ABE6690099F293 /* UTMAppleVirtualMachine.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE928C2926ABE6690099F293 /* UTMAppleVirtualMachine.swift */; };
CE928C3126ACCDEA0099F293 /* VMAppleRemovableDrivesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE928C3026ACCDEA0099F293 /* VMAppleRemovableDrivesView.swift */; };
CE93758924B930270074066F /* BusyOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE7D972B24B2B17D0080CB69 /* BusyOverlay.swift */; };
@@ -648,6 +658,19 @@
CE9A353426533A52005077CF /* JailbreakInterposer.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE9A352D26533A51005077CF /* JailbreakInterposer.framework */; platformFilter = ios; };
CE9A353526533A52005077CF /* JailbreakInterposer.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE9A352D26533A51005077CF /* JailbreakInterposer.framework */; platformFilter = ios; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
CE9A354026533AE6005077CF /* JailbreakInterposer.c in Sources */ = {isa = PBXBuildFile; fileRef = CE9A353F26533AE6005077CF /* JailbreakInterposer.c */; };
+ CE9B15362B11A491003A32DD /* SwiftConnect in Frameworks */ = {isa = PBXBuildFile; productRef = CE9B15352B11A491003A32DD /* SwiftConnect */; };
+ CE9B15382B11A4A7003A32DD /* SwiftConnect in Frameworks */ = {isa = PBXBuildFile; productRef = CE9B15372B11A4A7003A32DD /* SwiftConnect */; };
+ CE9B153A2B11A4AE003A32DD /* SwiftConnect in Frameworks */ = {isa = PBXBuildFile; productRef = CE9B15392B11A4AE003A32DD /* SwiftConnect */; };
+ CE9B153C2B11A4B4003A32DD /* SwiftConnect in Frameworks */ = {isa = PBXBuildFile; productRef = CE9B153B2B11A4B4003A32DD /* SwiftConnect */; };
+ CE9B153F2B11A63E003A32DD /* UTMRemoteServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE9B153E2B11A63E003A32DD /* UTMRemoteServer.swift */; };
+ CE9B15412B11A74E003A32DD /* UTMRemoteKeyManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE9B15402B11A74E003A32DD /* UTMRemoteKeyManager.swift */; };
+ CE9B15422B11A74E003A32DD /* UTMRemoteKeyManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE9B15402B11A74E003A32DD /* UTMRemoteKeyManager.swift */; };
+ CE9B15432B11A74E003A32DD /* UTMRemoteKeyManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE9B15402B11A74E003A32DD /* UTMRemoteKeyManager.swift */; };
+ CE9B15442B11A74E003A32DD /* UTMRemoteKeyManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE9B15402B11A74E003A32DD /* UTMRemoteKeyManager.swift */; };
+ CE9B15472B12A87E003A32DD /* GenerateKey.c in Sources */ = {isa = PBXBuildFile; fileRef = CE9B15462B12A87E003A32DD /* GenerateKey.c */; };
+ CE9B15482B12A87E003A32DD /* GenerateKey.c in Sources */ = {isa = PBXBuildFile; fileRef = CE9B15462B12A87E003A32DD /* GenerateKey.c */; };
+ CE9B15492B12A87E003A32DD /* GenerateKey.c in Sources */ = {isa = PBXBuildFile; fileRef = CE9B15462B12A87E003A32DD /* GenerateKey.c */; };
+ CE9B154A2B12A87E003A32DD /* GenerateKey.c in Sources */ = {isa = PBXBuildFile; fileRef = CE9B15462B12A87E003A32DD /* GenerateKey.c */; };
CEA45E25263519B5002FA97D /* VMDisplayMetalViewController+Pointer.h in Sources */ = {isa = PBXBuildFile; fileRef = 83FBDD53242FA71900D2C5D7 /* VMDisplayMetalViewController+Pointer.h */; };
CEA45E27263519B5002FA97D /* VMRemovableDrivesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE2D954524AD4F980059923A /* VMRemovableDrivesView.swift */; };
CEA45E37263519B5002FA97D /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE772AAB25C8B0F600E4E379 /* ContentView.swift */; };
@@ -692,7 +715,6 @@
CEA45ED1263519B5002FA97D /* UTMLocationManager.m in Sources */ = {isa = PBXBuildFile; fileRef = CE059DC7243E9E3400338317 /* UTMLocationManager.m */; platformFilter = ios; };
CEA45ED3263519B5002FA97D /* VMSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE2D954C24AD4F980059923A /* VMSettingsView.swift */; };
CEA45ED8263519B5002FA97D /* VMKeyboardButton.m in Sources */ = {isa = PBXBuildFile; fileRef = CEEB66452284B942002737B2 /* VMKeyboardButton.m */; };
- CEA45EDF263519B5002FA97D /* UTMJailbreak.m in Sources */ = {isa = PBXBuildFile; fileRef = CEB63A7924F469E300CAF323 /* UTMJailbreak.m */; };
CEA45EE8263519B5002FA97D /* VMDisplayMetalViewController+Keyboard.m in Sources */ = {isa = PBXBuildFile; fileRef = CE3ADD66240EFBCA002D6A5F /* VMDisplayMetalViewController+Keyboard.m */; };
CEA45EEA263519B5002FA97D /* UTMExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE2D954624AD4F980059923A /* UTMExtensions.swift */; };
CEA45EEC263519B5002FA97D /* UTMData.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE020BA224AEDC7C00B44AB6 /* UTMData.swift */; };
@@ -833,6 +855,7 @@
CEA9059625FC6A3B00801E7C /* usbredirparser.1.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CEA9058825FC69D100801E7C /* usbredirparser.1.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
CEB20EEA282053320033EFB5 /* DoubleClickHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEB20EE9282053320033EFB5 /* DoubleClickHandler.swift */; };
CEB54C852931E32F000D2AA9 /* UTMPatches.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEB54C802931C43F000D2AA9 /* UTMPatches.swift */; };
+ CEB5C1172B8C4CD4008AAE5C /* Info-RemotePlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = CEB5C1192B8C4CD4008AAE5C /* Info-RemotePlist.strings */; };
CEB63A7624F4654400CAF323 /* Main.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEB63A7524F4654400CAF323 /* Main.swift */; };
CEB63A7724F4654400CAF323 /* Main.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEB63A7524F4654400CAF323 /* Main.swift */; };
CEB63A7A24F469E300CAF323 /* UTMJailbreak.m in Sources */ = {isa = PBXBuildFile; fileRef = CEB63A7924F469E300CAF323 /* UTMJailbreak.m */; };
@@ -867,13 +890,25 @@
CED8DF7528A120C100C34345 /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = CED8DF7928A120C100C34345 /* Localizable.stringsdict */; };
CED8DF7628A120C100C34345 /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = CED8DF7928A120C100C34345 /* Localizable.stringsdict */; };
CED8DF7728A120C100C34345 /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = CED8DF7928A120C100C34345 /* Localizable.stringsdict */; };
+ CEDD11C12B7C74D7004DDAC6 /* SwiftPortmap in Frameworks */ = {isa = PBXBuildFile; productRef = CEDD11C02B7C74D7004DDAC6 /* SwiftPortmap */; };
CEDF83F9258AE24E0030E4AC /* UTMPasteboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEDF83F8258AE24E0030E4AC /* UTMPasteboard.swift */; };
CEDF83FA258AE24E0030E4AC /* UTMPasteboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEDF83F8258AE24E0030E4AC /* UTMPasteboard.swift */; };
+ CEE06B272B2FC89400A811AE /* UTMServerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEE06B262B2FC89400A811AE /* UTMServerView.swift */; };
+ CEE06B292B30013500A811AE /* UTMRemoteConnectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEE06B282B30013500A811AE /* UTMRemoteConnectView.swift */; };
CEE7E936287CFDB100282049 /* UTMLegacyQemuConfiguration+Constants.m in Sources */ = {isa = PBXBuildFile; fileRef = CEE7E934287CFDB100282049 /* UTMLegacyQemuConfiguration+Constants.m */; };
CEE7E937287CFDB100282049 /* UTMLegacyQemuConfiguration+Constants.m in Sources */ = {isa = PBXBuildFile; fileRef = CEE7E934287CFDB100282049 /* UTMLegacyQemuConfiguration+Constants.m */; };
CEE7E938287CFDB100282049 /* UTMLegacyQemuConfiguration+Constants.m in Sources */ = {isa = PBXBuildFile; fileRef = CEE7E934287CFDB100282049 /* UTMLegacyQemuConfiguration+Constants.m */; };
+ CEE8B4C22B71E0FB0035AE86 /* UTMLogging.m in Sources */ = {isa = PBXBuildFile; fileRef = CE6EDCE1241DA0E900A719DC /* UTMLogging.m */; };
+ CEE8B4C32B71E2BA0035AE86 /* UTMLoggingSwift.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE020BAA24AEE00000B44AB6 /* UTMLoggingSwift.swift */; };
CEEC811B24E48EC700ACB0B3 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEEC811A24E48EC600ACB0B3 /* SettingsView.swift */; };
CEECE13C25E47D9500A2AAB8 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEECE13B25E47D9500A2AAB8 /* AppDelegate.swift */; };
+ CEF01DB22B6724A300725A0F /* UTMSpiceVirtualMachine.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEF01DB12B6724A300725A0F /* UTMSpiceVirtualMachine.swift */; };
+ CEF01DB32B6724A300725A0F /* UTMSpiceVirtualMachine.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEF01DB12B6724A300725A0F /* UTMSpiceVirtualMachine.swift */; };
+ CEF01DB42B6724A300725A0F /* UTMSpiceVirtualMachine.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEF01DB12B6724A300725A0F /* UTMSpiceVirtualMachine.swift */; };
+ CEF01DB52B6724A300725A0F /* UTMSpiceVirtualMachine.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEF01DB12B6724A300725A0F /* UTMSpiceVirtualMachine.swift */; };
+ CEF01DB72B674BF000725A0F /* UTMPipeInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEF01DB62B674BF000725A0F /* UTMPipeInterface.swift */; };
+ CEF01DB82B674BF000725A0F /* UTMPipeInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEF01DB62B674BF000725A0F /* UTMPipeInterface.swift */; };
+ CEF01DB92B674BF000725A0F /* UTMPipeInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEF01DB62B674BF000725A0F /* UTMPipeInterface.swift */; };
CEF0300826A25A6900667B63 /* VMWizardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEF0300526A25A6900667B63 /* VMWizardView.swift */; };
CEF0304E26A2AFBE00667B63 /* BigButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEF0304C26A2AFBE00667B63 /* BigButtonStyle.swift */; };
CEF0304F26A2AFBF00667B63 /* BigButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEF0304C26A2AFBE00667B63 /* BigButtonStyle.swift */; };
@@ -913,6 +948,249 @@
CEF78EEB26B99F530022CAF4 /* GLESv2.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE5451A326AF5F0F008594E5 /* GLESv2.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
CEF78EEF26B9B7870022CAF4 /* virglrenderer.1.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE5451A126AF5F0F008594E5 /* virglrenderer.1.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
CEF78EF026B9B7910022CAF4 /* virglrenderer.1.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE5451A126AF5F0F008594E5 /* virglrenderer.1.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+ CEF7F5972AEEDCC400E34952 /* VMDisplayMetalViewController+Pointer.h in Sources */ = {isa = PBXBuildFile; fileRef = 83FBDD53242FA71900D2C5D7 /* VMDisplayMetalViewController+Pointer.h */; };
+ CEF7F5982AEEDCC400E34952 /* VMSettingsAddDeviceMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84C4D9012880CA8A00EC3B2B /* VMSettingsAddDeviceMenuView.swift */; };
+ CEF7F5992AEEDCC400E34952 /* VMRemovableDrivesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE2D954524AD4F980059923A /* VMRemovableDrivesView.swift */; };
+ CEF7F59A2AEEDCC400E34952 /* UTMQemuConfigurationDrive.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8443EFF12845641600B2E6E2 /* UTMQemuConfigurationDrive.swift */; };
+ CEF7F59B2AEEDCC400E34952 /* UTMQemuConfigurationSharing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8443EFF928456F3B00B2E6E2 /* UTMQemuConfigurationSharing.swift */; };
+ CEF7F59C2AEEDCC400E34952 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE772AAB25C8B0F600E4E379 /* ContentView.swift */; };
+ CEF7F59D2AEEDCC400E34952 /* VMData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 847BF9A92A49C783000BD9AA /* VMData.swift */; };
+ CEF7F59E2AEEDCC400E34952 /* UTMLegacyQemuConfiguration+System.m in Sources */ = {isa = PBXBuildFile; fileRef = CE5425332437C22A00E520F7 /* UTMLegacyQemuConfiguration+System.m */; };
+ CEF7F59F2AEEDCC400E34952 /* UTMQemuConfigurationNetwork.swift in Sources */ = {isa = PBXBuildFile; fileRef = 843BF82F2844853E0029D60D /* UTMQemuConfigurationNetwork.swift */; };
+ CEF7F5A12AEEDCC400E34952 /* VMWizardDrivesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEBE820226A4C1B5007AAB12 /* VMWizardDrivesView.swift */; };
+ CEF7F5A22AEEDCC400E34952 /* VMWindowState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84018688288A44C20050AC51 /* VMWindowState.swift */; };
+ CEF7F5A42AEEDCC400E34952 /* UTMLegacyQemuConfigurationPortForward.m in Sources */ = {isa = PBXBuildFile; fileRef = CE54252D2436E48D00E520F7 /* UTMLegacyQemuConfigurationPortForward.m */; };
+ CEF7F5A52AEEDCC400E34952 /* VMWizardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEF0307026A2B04300667B63 /* VMWizardView.swift */; };
+ CEF7F5A62AEEDCC400E34952 /* UTMPlaceholderVMView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84909A8C27CACD5C005605F1 /* UTMPlaceholderVMView.swift */; };
+ CEF7F5A72AEEDCC400E34952 /* BusyOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE7D972B24B2B17D0080CB69 /* BusyOverlay.swift */; };
+ CEF7F5A82AEEDCC400E34952 /* UTMConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848A98C3286F332D006F0550 /* UTMConfiguration.swift */; };
+ CEF7F5A92AEEDCC400E34952 /* UTMConfigurationInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 841619A9284315F9000034B2 /* UTMConfigurationInfo.swift */; };
+ CEF7F5AA2AEEDCC400E34952 /* VMConfigDisplayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE2D953724AD4F980059923A /* VMConfigDisplayView.swift */; };
+ CEF7F5AB2AEEDCC400E34952 /* VMWizardOSWindowsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEF0305626A2AFDD00667B63 /* VMWizardOSWindowsView.swift */; };
+ CEF7F5AC2AEEDCC400E34952 /* UTMDownloadTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83A004B826A8CC95001AC09E /* UTMDownloadTask.swift */; };
+ CEF7F5AD2AEEDCC400E34952 /* UTMApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 841E58D02893AF5400137A20 /* UTMApp.swift */; platformFilter = ios; };
+ CEF7F5AE2AEEDCC400E34952 /* VMConfigAdvancedNetworkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85EC516327CC8C98004A51DE /* VMConfigAdvancedNetworkView.swift */; };
+ CEF7F5AF2AEEDCC400E34952 /* UTMLegacyQemuConfiguration+Miscellaneous.m in Sources */ = {isa = PBXBuildFile; fileRef = CEE0421124418F2E0001680F /* UTMLegacyQemuConfiguration+Miscellaneous.m */; };
+ CEF7F5B02AEEDCC400E34952 /* UTMRegistryEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 841E997828AA119B003C6CB6 /* UTMRegistryEntry.swift */; };
+ CEF7F5B12AEEDCC400E34952 /* UTMDataExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEBBF1A424B56A2900C15049 /* UTMDataExtension.swift */; };
+ CEF7F5B22AEEDCC400E34952 /* VMDisplayHostedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CF5DD2288DCE6400D01721 /* VMDisplayHostedView.swift */; };
+ CEF7F5B32AEEDCC400E34952 /* QEMUArgumentBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848D99BF2866D9CE0055C215 /* QEMUArgumentBuilder.swift */; };
+ CEF7F5B42AEEDCC400E34952 /* ImagePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED814EE24C7EB760042F0F1 /* ImagePicker.swift */; };
+ CEF7F5B52AEEDCC400E34952 /* VMConfigSystemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE2D955324AD4F980059923A /* VMConfigSystemView.swift */; };
+ CEF7F5B62AEEDCC400E34952 /* FileBrowseField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8432329328C2ED9000CFBC97 /* FileBrowseField.swift */; };
+ CEF7F5B72AEEDCC400E34952 /* VMShareFileModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE8813D424CD265700532628 /* VMShareFileModifier.swift */; };
+ CEF7F5B82AEEDCC400E34952 /* UTMQemuConfigurationSerial.swift in Sources */ = {isa = PBXBuildFile; fileRef = 843BF83B2845494C0029D60D /* UTMQemuConfigurationSerial.swift */; };
+ CEF7F5B92AEEDCC400E34952 /* Spinner.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEF0304D26A2AFBE00667B63 /* Spinner.swift */; };
+ CEF7F5BA2AEEDCC400E34952 /* UTMQemuConfigurationPortForward.swift in Sources */ = {isa = PBXBuildFile; fileRef = 843BF83F284555E70029D60D /* UTMQemuConfigurationPortForward.swift */; };
+ CEF7F5BB2AEEDCC400E34952 /* UTMReleaseHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE611BE629F50CAD001817BC /* UTMReleaseHelper.swift */; };
+ CEF7F5BC2AEEDCC400E34952 /* VMConfigNetworkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE2D955024AD4F980059923A /* VMConfigNetworkView.swift */; };
+ CEF7F5BD2AEEDCC400E34952 /* UTMLegacyViewState.m in Sources */ = {isa = PBXBuildFile; fileRef = CE6EDCDE241C4A6800A719DC /* UTMLegacyViewState.m */; };
+ CEF7F5BF2AEEDCC400E34952 /* VMWizardOSLinuxView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEF0305726A2AFDE00667B63 /* VMWizardOSLinuxView.swift */; };
+ CEF7F5C02AEEDCC400E34952 /* VMWizardSummaryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEBE820A26A4C8E0007AAB12 /* VMWizardSummaryView.swift */; };
+ CEF7F5C12AEEDCC400E34952 /* VMConfigQEMUView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE2D953924AD4F980059923A /* VMConfigQEMUView.swift */; };
+ CEF7F5C22AEEDCC400E34952 /* VMDisplayMetalViewController+Touch.m in Sources */ = {isa = PBXBuildFile; fileRef = CE056CA5242454100004B68A /* VMDisplayMetalViewController+Touch.m */; };
+ CEF7F5C32AEEDCC400E34952 /* UTMLegacyQemuConfiguration+Display.m in Sources */ = {isa = PBXBuildFile; fileRef = CEE0420B244117040001680F /* UTMLegacyQemuConfiguration+Display.m */; };
+ CEF7F5C42AEEDCC400E34952 /* BigButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEF0304C26A2AFBE00667B63 /* BigButtonStyle.swift */; };
+ CEF7F5C52AEEDCC400E34952 /* UTMVirtualMachine.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE020BB524B14F8400B44AB6 /* UTMVirtualMachine.swift */; };
+ CEF7F5C62AEEDCC400E34952 /* UTMQemuConfigurationSystem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 841619AD28431952000034B2 /* UTMQemuConfigurationSystem.swift */; };
+ CEF7F5C72AEEDCC400E34952 /* VMWizardSharingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEBE820626A4C74E007AAB12 /* VMWizardSharingView.swift */; };
+ CEF7F5C82AEEDCC400E34952 /* VMConfigInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED814EB24C7C2850042F0F1 /* VMConfigInfoView.swift */; };
+ CEF7F5C92AEEDCC400E34952 /* MenuLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F909FE289488F90008DBE2 /* MenuLabel.swift */; };
+ CEF7F5CA2AEEDCC400E34952 /* VMKeyboardView.m in Sources */ = {isa = PBXBuildFile; fileRef = CE4507D1226A5BE200A28D22 /* VMKeyboardView.m */; };
+ CEF7F5CB2AEEDCC400E34952 /* VMWizardOSView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEF0305A26A2AFDE00667B63 /* VMWizardOSView.swift */; };
+ CEF7F5CC2AEEDCC400E34952 /* DestructiveButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B36D2827B790BE00C22685 /* DestructiveButton.swift */; };
+ CEF7F5CD2AEEDCC400E34952 /* UTMConfigurationTerminal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 843BF83728451B380029D60D /* UTMConfigurationTerminal.swift */; };
+ CEF7F5CE2AEEDCC400E34952 /* VMWindowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84018682288A3B2E0050AC51 /* VMWindowView.swift */; };
+ CEF7F5CF2AEEDCC400E34952 /* UTMPendingVMView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83034C0626AB630F006B4BAF /* UTMPendingVMView.swift */; };
+ CEF7F5D02AEEDCC400E34952 /* UTMSpiceIO.m in Sources */ = {isa = PBXBuildFile; fileRef = E2D64BC8241DB24B0034E0C6 /* UTMSpiceIO.m */; };
+ CEF7F5D12AEEDCC400E34952 /* UTMUnavailableVMView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84909A9027CADAE0005605F1 /* UTMUnavailableVMView.swift */; };
+ CEF7F5D22AEEDCC400E34952 /* VMDrivesSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE2D955124AD4F980059923A /* VMDrivesSettingsView.swift */; };
+ CEF7F5D32AEEDCC400E34952 /* UTMConfigurationDrive.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848D99BB28636AC90055C215 /* UTMConfigurationDrive.swift */; };
+ CEF7F5D42AEEDCC400E34952 /* VMConfigDriveCreateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED814E824C79F070042F0F1 /* VMConfigDriveCreateView.swift */; };
+ CEF7F5D52AEEDCC400E34952 /* UTMPatches.swift in Sources */ = {isa = PBXBuildFile; fileRef = 842B9F8C28CC58B700031EE7 /* UTMPatches.swift */; };
+ CEF7F5D62AEEDCC400E34952 /* RAMSlider.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE19392526DCB093005CEC17 /* RAMSlider.swift */; };
+ CEF7F5D72AEEDCC400E34952 /* VMReleaseNotesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE611BEA29F50D3E001817BC /* VMReleaseNotesView.swift */; };
+ CEF7F5D82AEEDCC400E34952 /* UTMLegacyQemuConfiguration+Constants.m in Sources */ = {isa = PBXBuildFile; fileRef = CEE7E934287CFDB100282049 /* UTMLegacyQemuConfiguration+Constants.m */; };
+ CEF7F5D92AEEDCC400E34952 /* InListButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B224B9C279D4D8100B63CFF /* InListButtonStyle.swift */; };
+ CEF7F5DA2AEEDCC400E34952 /* VMContextMenuModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C33B3A82566C9B100A954A6 /* VMContextMenuModifier.swift */; };
+ CEF7F5DB2AEEDCC400E34952 /* VMDisplayMetalViewController+Pencil.m in Sources */ = {isa = PBXBuildFile; fileRef = CE5076DA250AB55D00C26C19 /* VMDisplayMetalViewController+Pencil.m */; platformFilter = ios; };
+ CEF7F5DC2AEEDCC400E34952 /* VMDisplayTerminalViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8401865D2887B1620050AC51 /* VMDisplayTerminalViewController.swift */; };
+ CEF7F5DD2AEEDCC400E34952 /* UTMLegacyQemuConfiguration+Drives.m in Sources */ = {isa = PBXBuildFile; fileRef = CE5425302437C09C00E520F7 /* UTMLegacyQemuConfiguration+Drives.m */; };
+ CEF7F5DE2AEEDCC400E34952 /* UTMPendingVirtualMachine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 835AA7B026AB7C85007A0411 /* UTMPendingVirtualMachine.swift */; };
+ CEF7F5DF2AEEDCC400E34952 /* BusyIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84018696288B71BF0050AC51 /* BusyIndicator.swift */; };
+ CEF7F5E02AEEDCC400E34952 /* VMSessionState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84018685288A3B5B0050AC51 /* VMSessionState.swift */; };
+ CEF7F5E12AEEDCC400E34952 /* VMConfigSharingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE2D954724AD4F980059923A /* VMConfigSharingView.swift */; };
+ CEF7F5E22AEEDCC400E34952 /* VMConfigInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE2D954824AD4F980059923A /* VMConfigInputView.swift */; };
+ CEF7F5E32AEEDCC400E34952 /* VMWizardOSOtherView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEF0305426A2AFDD00667B63 /* VMWizardOSOtherView.swift */; };
+ CEF7F5E42AEEDCC400E34952 /* VMToolbarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84C60FB62681A41B00B58C00 /* VMToolbarView.swift */; };
+ CEF7F5E52AEEDCC400E34952 /* VMDisplayMetalViewController+Gamepad.m in Sources */ = {isa = PBXBuildFile; fileRef = 5286EC8F2437488E007E6CBC /* VMDisplayMetalViewController+Gamepad.m */; };
+ CEF7F5E62AEEDCC400E34952 /* VMWizardHardwareView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEF0307326A2B40B00667B63 /* VMWizardHardwareView.swift */; };
+ CEF7F5E72AEEDCC400E34952 /* UTMRegistry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 841E997428AA1191003C6CB6 /* UTMRegistry.swift */; };
+ CEF7F5E82AEEDCC400E34952 /* VMDisplayViewControllerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8401868E288A50B90050AC51 /* VMDisplayViewControllerDelegate.swift */; };
+ CEF7F5EA2AEEDCC400E34952 /* VMConfigConstantPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848D99A7285DB5550055C215 /* VMConfigConstantPicker.swift */; };
+ CEF7F5EC2AEEDCC400E34952 /* VMToolbarModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE2D953824AD4F980059923A /* VMToolbarModifier.swift */; };
+ CEF7F5ED2AEEDCC400E34952 /* VMCursor.m in Sources */ = {isa = PBXBuildFile; fileRef = CE3ADD692411C661002D6A5F /* VMCursor.m */; };
+ CEF7F5EE2AEEDCC400E34952 /* VMConfigDriveDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE9375A024BBDDD10074066F /* VMConfigDriveDetailsView.swift */; };
+ CEF7F5F02AEEDCC400E34952 /* NumberTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED234EC254796E500ED0A57 /* NumberTextField.swift */; };
+ CEF7F5F12AEEDCC400E34952 /* VMToolbarOrnamentModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEA51F862A81EAB700DDD7FA /* VMToolbarOrnamentModifier.swift */; platformFilters = (xros, ); };
+ CEF7F5F22AEEDCC400E34952 /* VMCommands.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE772AB225C8B7B500E4E379 /* VMCommands.swift */; };
+ CEF7F5F32AEEDCC400E34952 /* UTMLegacyQemuConfiguration+Networking.m in Sources */ = {isa = PBXBuildFile; fileRef = CEA02A982436C7A30087E45F /* UTMLegacyQemuConfiguration+Networking.m */; };
+ CEF7F5F42AEEDCC400E34952 /* VMConfirmActionModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE6D21DB2553A6ED001D29C5 /* VMConfirmActionModifier.swift */; };
+ CEF7F5F52AEEDCC400E34952 /* QEMUConstant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 841619B52843226B000034B2 /* QEMUConstant.swift */; };
+ CEF7F5F62AEEDCC400E34952 /* VMConfigPortForwardForm.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE2D955224AD4F980059923A /* VMConfigPortForwardForm.swift */; };
+ CEF7F5F82AEEDCC400E34952 /* DetailedSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8471772727CD3CAB00D3A50B /* DetailedSection.swift */; };
+ CEF7F5F92AEEDCC400E34952 /* VMToolbarDriveMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CF5DF4288F558400D01721 /* VMToolbarDriveMenuView.swift */; };
+ CEF7F5FA2AEEDCC400E34952 /* VMSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE2D954C24AD4F980059923A /* VMSettingsView.swift */; };
+ CEF7F5FB2AEEDCC400E34952 /* VMDisplayViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84C60FB9268269D700B58C00 /* VMDisplayViewController.swift */; };
+ CEF7F5FC2AEEDCC400E34952 /* VMWizardStartView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEF0305926A2AFDE00667B63 /* VMWizardStartView.swift */; };
+ CEF7F5FD2AEEDCC400E34952 /* QEMUConstantGenerated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 843BF82728441FAF0029D60D /* QEMUConstantGenerated.swift */; };
+ CEF7F5FE2AEEDCC400E34952 /* VMKeyboardButton.m in Sources */ = {isa = PBXBuildFile; fileRef = CEEB66452284B942002737B2 /* VMKeyboardButton.m */; };
+ CEF7F5FF2AEEDCC400E34952 /* UTMDownloadVMTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B36D2427B704C200C22685 /* UTMDownloadVMTask.swift */; };
+ CEF7F6002AEEDCC400E34952 /* GlobalFileImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8432329728C3017F00CFBC97 /* GlobalFileImporter.swift */; };
+ CEF7F6022AEEDCC400E34952 /* VMWizardContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84C2E8642AA429E800B17308 /* VMWizardContent.swift */; };
+ CEF7F6032AEEDCC400E34952 /* UTMExternalSceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 841E58CA28937EE200137A20 /* UTMExternalSceneDelegate.swift */; platformFilter = ios; };
+ CEF7F6052AEEDCC400E34952 /* UTMQemuConfigurationQEMU.swift in Sources */ = {isa = PBXBuildFile; fileRef = 841619B128431DA5000034B2 /* UTMQemuConfigurationQEMU.swift */; };
+ CEF7F6062AEEDCC400E34952 /* UTMQemuConfigurationDisplay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 843BF82328441EAD0029D60D /* UTMQemuConfigurationDisplay.swift */; };
+ CEF7F6072AEEDCC400E34952 /* UTMApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE80111F2AD4E9E8009001C2 /* UTMApp.swift */; platformFilters = (xros, ); };
+ CEF7F6082AEEDCC400E34952 /* VMConfigDisplayConsoleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8401FDA5269D44E400265F0D /* VMConfigDisplayConsoleView.swift */; };
+ CEF7F60A2AEEDCC400E34952 /* VMConfigSerialView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848D99B728630A780055C215 /* VMConfigSerialView.swift */; };
+ CEF7F60B2AEEDCC400E34952 /* VMWizardState.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEF0305526A2AFDD00667B63 /* VMWizardState.swift */; };
+ CEF7F60C2AEEDCC400E34952 /* UTMQemuConfigurationInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 843BF82B284482C10029D60D /* UTMQemuConfigurationInput.swift */; };
+ CEF7F60D2AEEDCC400E34952 /* VMDisplayMetalViewController+Keyboard.m in Sources */ = {isa = PBXBuildFile; fileRef = CE3ADD66240EFBCA002D6A5F /* VMDisplayMetalViewController+Keyboard.m */; };
+ CEF7F60E2AEEDCC400E34952 /* UTMExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE2D954624AD4F980059923A /* UTMExtensions.swift */; };
+ CEF7F60F2AEEDCC400E34952 /* UTMData.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE020BA224AEDC7C00B44AB6 /* UTMData.swift */; };
+ CEF7F6112AEEDCC400E34952 /* VMConfigSoundView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE2D953A24AD4F980059923A /* VMConfigSoundView.swift */; };
+ CEF7F6122AEEDCC400E34952 /* UTMLegacyQemuConfiguration.m in Sources */ = {isa = PBXBuildFile; fileRef = CE31C244225E555600A965DD /* UTMLegacyQemuConfiguration.m */; };
+ CEF7F6142AEEDCC400E34952 /* VMDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE2D954B24AD4F980059923A /* VMDetailsView.swift */; };
+ CEF7F6152AEEDCC400E34952 /* VMDisplayMetalViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 5286EC94243748C3007E6CBC /* VMDisplayMetalViewController.m */; };
+ CEF7F6162AEEDCC400E34952 /* UTMQemuConfiguration+Arguments.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848D99C328670F650055C215 /* UTMQemuConfiguration+Arguments.swift */; };
+ CEF7F6172AEEDCC400E34952 /* Main.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEB63A7524F4654400CAF323 /* Main.swift */; };
+ CEF7F6192AEEDCC400E34952 /* VMCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE2D954324AD4F980059923A /* VMCardView.swift */; };
+ CEF7F61A2AEEDCC400E34952 /* VMNavigationListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8432328F28C2CDAD00CFBC97 /* VMNavigationListView.swift */; };
+ CEF7F61B2AEEDCC400E34952 /* UTMSingleWindowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 841E58CD28937FED00137A20 /* UTMSingleWindowView.swift */; };
+ CEF7F61C2AEEDCC400E34952 /* UTMLegacyQemuConfiguration+Sharing.m in Sources */ = {isa = PBXBuildFile; fileRef = CE059DC4243BFA3200338317 /* UTMLegacyQemuConfiguration+Sharing.m */; };
+ CEF7F61D2AEEDCC400E34952 /* SizeTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84C505AB28C588EC007CE8FF /* SizeTextField.swift */; };
+ CEF7F61E2AEEDCC400E34952 /* DefaultTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8471770527CC974F00D3A50B /* DefaultTextField.swift */; };
+ CEF7F61F2AEEDCC400E34952 /* VMToolbarDisplayMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E6F6FC289319AE00080EEF /* VMToolbarDisplayMenuView.swift */; };
+ CEF7F6202AEEDCC400E34952 /* ActivityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE8813D224CD230300532628 /* ActivityView.swift */; };
+ CEF7F6212AEEDCC400E34952 /* UTMPasteboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEDF83F8258AE24E0030E4AC /* UTMPasteboard.swift */; };
+ CEF7F6222AEEDCC400E34952 /* QEMUArgument.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848D99B3286300160055C215 /* QEMUArgument.swift */; };
+ CEF7F6232AEEDCC400E34952 /* VMPlaceholderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE2D954224AD4F980059923A /* VMPlaceholderView.swift */; };
+ CEF7F6242AEEDCC400E34952 /* VMDisplayMetalViewController+Pointer.m in Sources */ = {isa = PBXBuildFile; fileRef = 83FBDD55242FA7BC00D2C5D7 /* VMDisplayMetalViewController+Pointer.m */; };
+ CEF7F6252AEEDCC400E34952 /* VMDisplayViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = CE72B4AC2463579D00716A11 /* VMDisplayViewController.m */; };
+ CEF7F6282AEEDCC400E34952 /* UTMQemuConfigurationSound.swift in Sources */ = {isa = PBXBuildFile; fileRef = 843BF83328450C0B0029D60D /* UTMQemuConfigurationSound.swift */; };
+ CEF7F6292AEEDCC400E34952 /* VMScroll.m in Sources */ = {isa = PBXBuildFile; fileRef = CE20FAE72448D2BE0059AE11 /* VMScroll.m */; };
+ CEF7F62A2AEEDCC400E34952 /* VMConfigNetworkPortForwardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE2D954D24AD4F980059923A /* VMConfigNetworkPortForwardView.swift */; };
+ CEF7F62B2AEEDCC400E34952 /* UTMDownloadSupportToolsTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 843232B628C4816100CFBC97 /* UTMDownloadSupportToolsTask.swift */; };
+ CEF7F62C2AEEDCC400E34952 /* UTMQemuConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 841619A5284315C1000034B2 /* UTMQemuConfiguration.swift */; };
+ CEF7F62E2AEEDCC400E34952 /* libgstautodetect.a in Frameworks */ = {isa = PBXBuildFile; fileRef = CE9D19522265425900355E14 /* libgstautodetect.a */; };
+ CEF7F62F2AEEDCC400E34952 /* libgstaudiotestsrc.a in Frameworks */ = {isa = PBXBuildFile; fileRef = CE9D19532265425900355E14 /* libgstaudiotestsrc.a */; };
+ CEF7F6302AEEDCC400E34952 /* libgstvideoconvert.a in Frameworks */ = {isa = PBXBuildFile; fileRef = CE9D19542265425900355E14 /* libgstvideoconvert.a */; };
+ CEF7F6312AEEDCC400E34952 /* libgstaudioconvert.a in Frameworks */ = {isa = PBXBuildFile; fileRef = CE9D19552265425900355E14 /* libgstaudioconvert.a */; };
+ CEF7F6322AEEDCC400E34952 /* libgstvideoscale.a in Frameworks */ = {isa = PBXBuildFile; fileRef = CE9D19562265425900355E14 /* libgstvideoscale.a */; };
+ CEF7F6332AEEDCC400E34952 /* IQKeyboardManagerSwift in Frameworks */ = {isa = PBXBuildFile; platformFilter = ios; productRef = CEF7F5862AEEDCC400E34952 /* IQKeyboardManagerSwift */; };
+ CEF7F6342AEEDCC400E34952 /* MetalKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE66450C2269313200B0849A /* MetalKit.framework */; };
+ CEF7F6352AEEDCC400E34952 /* libgstvolume.a in Frameworks */ = {isa = PBXBuildFile; fileRef = CE9D19572265425900355E14 /* libgstvolume.a */; };
+ CEF7F6362AEEDCC400E34952 /* libgstcoreelements.a in Frameworks */ = {isa = PBXBuildFile; fileRef = CE9D19582265425900355E14 /* libgstcoreelements.a */; };
+ CEF7F6372AEEDCC400E34952 /* libgstvideorate.a in Frameworks */ = {isa = PBXBuildFile; fileRef = CE9D19592265425900355E14 /* libgstvideorate.a */; };
+ CEF7F6382AEEDCC400E34952 /* libgstjpeg.a in Frameworks */ = {isa = PBXBuildFile; fileRef = CE9D195A2265425900355E14 /* libgstjpeg.a */; };
+ CEF7F6392AEEDCC400E34952 /* libgstaudioresample.a in Frameworks */ = {isa = PBXBuildFile; fileRef = CE9D195B2265425900355E14 /* libgstaudioresample.a */; };
+ CEF7F63A2AEEDCC400E34952 /* libgstplayback.a in Frameworks */ = {isa = PBXBuildFile; fileRef = CE9D195C2265425900355E14 /* libgstplayback.a */; };
+ CEF7F63C2AEEDCC400E34952 /* libgstadder.a in Frameworks */ = {isa = PBXBuildFile; fileRef = CE9D195D2265425900355E14 /* libgstadder.a */; };
+ CEF7F63D2AEEDCC400E34952 /* libgstaudiorate.a in Frameworks */ = {isa = PBXBuildFile; fileRef = CE9D195F2265425900355E14 /* libgstaudiorate.a */; };
+ CEF7F63F2AEEDCC400E34952 /* libgstvideofilter.a in Frameworks */ = {isa = PBXBuildFile; fileRef = CE9D19602265425900355E14 /* libgstvideofilter.a */; };
+ CEF7F6402AEEDCC400E34952 /* SwiftUIVisualEffects in Frameworks */ = {isa = PBXBuildFile; productRef = CEF7F5902AEEDCC400E34952 /* SwiftUIVisualEffects */; };
+ CEF7F6422AEEDCC400E34952 /* libgstapp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = CE9D19612265425900355E14 /* libgstapp.a */; };
+ CEF7F6432AEEDCC400E34952 /* libgstgio.a in Frameworks */ = {isa = PBXBuildFile; fileRef = CE9D19622265425A00355E14 /* libgstgio.a */; };
+ CEF7F6442AEEDCC400E34952 /* libgsttypefindfunctions.a in Frameworks */ = {isa = PBXBuildFile; fileRef = CE9D19632265425A00355E14 /* libgsttypefindfunctions.a */; };
+ CEF7F6452AEEDCC400E34952 /* libgstvideotestsrc.a in Frameworks */ = {isa = PBXBuildFile; fileRef = CE9D19642265425A00355E14 /* libgstvideotestsrc.a */; };
+ CEF7F6462AEEDCC400E34952 /* libgstosxaudio.a in Frameworks */ = {isa = PBXBuildFile; fileRef = CE9D19652265425A00355E14 /* libgstosxaudio.a */; };
+ CEF7F6472AEEDCC400E34952 /* gmodule-2.0.0.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE2D63D822653C7300FC7E63 /* gmodule-2.0.0.framework */; };
+ CEF7F6482AEEDCC400E34952 /* jpeg.62.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE2D63D922653C7300FC7E63 /* jpeg.62.framework */; };
+ CEF7F6492AEEDCC400E34952 /* ZIPFoundation in Frameworks */ = {isa = PBXBuildFile; productRef = CEF7F5882AEEDCC400E34952 /* ZIPFoundation */; };
+ CEF7F64A2AEEDCC400E34952 /* intl.8.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE2D63DA22653C7300FC7E63 /* intl.8.framework */; };
+ CEF7F64B2AEEDCC400E34952 /* gstapp-1.0.0.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE2D63DB22653C7300FC7E63 /* gstapp-1.0.0.framework */; };
+ CEF7F64C2AEEDCC400E34952 /* gthread-2.0.0.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE2D63DC22653C7300FC7E63 /* gthread-2.0.0.framework */; };
+ CEF7F64D2AEEDCC400E34952 /* gstrtp-1.0.0.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE2D63DD22653C7400FC7E63 /* gstrtp-1.0.0.framework */; };
+ CEF7F64E2AEEDCC400E34952 /* gstriff-1.0.0.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE2D63DE22653C7400FC7E63 /* gstriff-1.0.0.framework */; };
+ CEF7F6512AEEDCC400E34952 /* AVFAudio.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 84818C0B2898A07A009EDB67 /* AVFAudio.framework */; };
+ CEF7F6522AEEDCC400E34952 /* gstreamer-1.0.0.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE2D63E022653C7400FC7E63 /* gstreamer-1.0.0.framework */; };
+ CEF7F6532AEEDCC400E34952 /* json-glib-1.0.0.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE2D63E222653C7400FC7E63 /* json-glib-1.0.0.framework */; };
+ CEF7F6542AEEDCC400E34952 /* ffi.7.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE2D63E322653C7400FC7E63 /* ffi.7.framework */; };
+ CEF7F6552AEEDCC400E34952 /* gstnet-1.0.0.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE2D63E522653C7400FC7E63 /* gstnet-1.0.0.framework */; };
+ CEF7F6562AEEDCC400E34952 /* gstbase-1.0.0.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE2D63E822653C7400FC7E63 /* gstbase-1.0.0.framework */; };
+ CEF7F6572AEEDCC400E34952 /* Logging in Frameworks */ = {isa = PBXBuildFile; productRef = CEF7F5842AEEDCC400E34952 /* Logging */; };
+ CEF7F6582AEEDCC400E34952 /* SwiftTerm in Frameworks */ = {isa = PBXBuildFile; productRef = CEF7F58E2AEEDCC400E34952 /* SwiftTerm */; };
+ CEF7F65A2AEEDCC400E34952 /* phodav-2.0.0.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE059DC0243BD67100338317 /* phodav-2.0.0.framework */; };
+ CEF7F65C2AEEDCC400E34952 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE0E9B86252FD06B0026E02B /* SwiftUI.framework */; settings = {ATTRIBUTES = (Weak, ); }; };
+ CEF7F65D2AEEDCC400E34952 /* gstcontroller-1.0.0.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE2D63EE22653C7400FC7E63 /* gstcontroller-1.0.0.framework */; };
+ CEF7F65E2AEEDCC400E34952 /* gstaudio-1.0.0.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE2D63EF22653C7400FC7E63 /* gstaudio-1.0.0.framework */; };
+ CEF7F65F2AEEDCC400E34952 /* gpg-error.0.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE2D63F122653C7400FC7E63 /* gpg-error.0.framework */; };
+ CEF7F6602AEEDCC400E34952 /* gcrypt.20.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE2D63F322653C7400FC7E63 /* gcrypt.20.framework */; };
+ CEF7F6612AEEDCC400E34952 /* InAppSettingsKit in Frameworks */ = {isa = PBXBuildFile; platformFilter = ios; productRef = CEF7F5922AEEDCC400E34952 /* InAppSettingsKit */; };
+ CEF7F6622AEEDCC400E34952 /* gobject-2.0.0.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE2D63F522653C7400FC7E63 /* gobject-2.0.0.framework */; };
+ CEF7F6642AEEDCC400E34952 /* gsttag-1.0.0.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE2D63F622653C7400FC7E63 /* gsttag-1.0.0.framework */; };
+ CEF7F6652AEEDCC400E34952 /* gio-2.0.0.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE2D63F822653C7400FC7E63 /* gio-2.0.0.framework */; };
+ CEF7F6662AEEDCC400E34952 /* gstvideo-1.0.0.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE2D63F922653C7400FC7E63 /* gstvideo-1.0.0.framework */; };
+ CEF7F6672AEEDCC400E34952 /* spice-client-glib-2.0.8.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE2D63FE22653C7500FC7E63 /* spice-client-glib-2.0.8.framework */; };
+ CEF7F6682AEEDCC400E34952 /* gstrtsp-1.0.0.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE2D640122653C7500FC7E63 /* gstrtsp-1.0.0.framework */; };
+ CEF7F6692AEEDCC400E34952 /* opus.0.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE2D640322653C7500FC7E63 /* opus.0.framework */; };
+ CEF7F66A2AEEDCC400E34952 /* glib-2.0.0.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE2D640422653C7500FC7E63 /* glib-2.0.0.framework */; };
+ CEF7F66B2AEEDCC400E34952 /* png16.16.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE2D640522653C7500FC7E63 /* png16.16.framework */; };
+ CEF7F66C2AEEDCC400E34952 /* gstfft-1.0.0.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE2D640922653C7500FC7E63 /* gstfft-1.0.0.framework */; };
+ CEF7F66D2AEEDCC400E34952 /* crypto.1.1.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE2D640A22653C7500FC7E63 /* crypto.1.1.framework */; };
+ CEF7F66E2AEEDCC400E34952 /* gstpbutils-1.0.0.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE2D640E22653C7500FC7E63 /* gstpbutils-1.0.0.framework */; };
+ CEF7F66F2AEEDCC400E34952 /* gstallocators-1.0.0.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE2D641122653C7500FC7E63 /* gstallocators-1.0.0.framework */; };
+ CEF7F6702AEEDCC400E34952 /* gstcheck-1.0.0.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE2D641422653C7500FC7E63 /* gstcheck-1.0.0.framework */; };
+ CEF7F6712AEEDCC400E34952 /* iconv.2.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE2D641522653C7500FC7E63 /* iconv.2.framework */; };
+ CEF7F6722AEEDCC400E34952 /* gstsdp-1.0.0.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE2D641622653C7500FC7E63 /* gstsdp-1.0.0.framework */; };
+ CEF7F6742AEEDCC400E34952 /* ssl.1.1.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE2D641722653C7500FC7E63 /* ssl.1.1.framework */; };
+ CEF7F6762AEEDCC400E34952 /* pixman-1.0.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE2D641922653C7600FC7E63 /* pixman-1.0.framework */; };
+ CEF7F6792AEEDCC400E34952 /* Icons in Resources */ = {isa = PBXBuildFile; fileRef = CE4698F824C8FBD9008C1BD6 /* Icons */; };
+ CEF7F67B2AEEDCC400E34952 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 521F3EFB2414F73800130500 /* Localizable.strings */; };
+ CEF7F67C2AEEDCC400E34952 /* qemu in Resources */ = {isa = PBXBuildFile; fileRef = CE9D18F72265410E00355E14 /* qemu */; };
+ CEF7F67D2AEEDCC400E34952 /* VMDisplayMetalViewInputAccessory.xib in Resources */ = {isa = PBXBuildFile; fileRef = CE061CE9289EB6250000351C /* VMDisplayMetalViewInputAccessory.xib */; };
+ CEF7F67E2AEEDCC400E34952 /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = CED8DF7928A120C100C34345 /* Localizable.stringsdict */; };
+ CEF7F67F2AEEDCC400E34952 /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = 5286EC91243748AC007E6CBC /* Settings.bundle */; };
+ CEF7F6802AEEDCC400E34952 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = CE550BD52259479D0063E575 /* Assets.xcassets */; };
+ CEF7F6832AEEDCC400E34952 /* gpg-error.0.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE2D63F122653C7400FC7E63 /* gpg-error.0.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
+ CEF7F6852AEEDCC400E34952 /* gstcontroller-1.0.0.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE2D63EE22653C7400FC7E63 /* gstcontroller-1.0.0.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
+ CEF7F6862AEEDCC400E34952 /* gstallocators-1.0.0.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE2D641122653C7500FC7E63 /* gstallocators-1.0.0.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
+ CEF7F6872AEEDCC400E34952 /* gstbase-1.0.0.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE2D63E822653C7400FC7E63 /* gstbase-1.0.0.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
+ CEF7F6882AEEDCC400E34952 /* ffi.7.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE2D63E322653C7400FC7E63 /* ffi.7.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
+ CEF7F6892AEEDCC400E34952 /* ssl.1.1.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE2D641722653C7500FC7E63 /* ssl.1.1.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
+ CEF7F68B2AEEDCC400E34952 /* gio-2.0.0.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE2D63F822653C7400FC7E63 /* gio-2.0.0.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
+ CEF7F68D2AEEDCC400E34952 /* png16.16.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE2D640522653C7500FC7E63 /* png16.16.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
+ CEF7F68E2AEEDCC400E34952 /* gstnet-1.0.0.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE2D63E522653C7400FC7E63 /* gstnet-1.0.0.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
+ CEF7F6902AEEDCC400E34952 /* crypto.1.1.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE2D640A22653C7500FC7E63 /* crypto.1.1.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
+ CEF7F6922AEEDCC400E34952 /* gstapp-1.0.0.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE2D63DB22653C7300FC7E63 /* gstapp-1.0.0.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
+ CEF7F6962AEEDCC400E34952 /* gsttag-1.0.0.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE2D63F622653C7400FC7E63 /* gsttag-1.0.0.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
+ CEF7F6982AEEDCC400E34952 /* gstrtp-1.0.0.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE2D63DD22653C7400FC7E63 /* gstrtp-1.0.0.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
+ CEF7F6992AEEDCC400E34952 /* gstriff-1.0.0.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE2D63DE22653C7400FC7E63 /* gstriff-1.0.0.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
+ CEF7F69C2AEEDCC400E34952 /* phodav-2.0.0.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE059DC0243BD67100338317 /* phodav-2.0.0.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
+ CEF7F69D2AEEDCC400E34952 /* gthread-2.0.0.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE2D63DC22653C7300FC7E63 /* gthread-2.0.0.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
+ CEF7F6A22AEEDCC400E34952 /* gobject-2.0.0.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE2D63F522653C7400FC7E63 /* gobject-2.0.0.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
+ CEF7F6A32AEEDCC400E34952 /* gmodule-2.0.0.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE2D63D822653C7300FC7E63 /* gmodule-2.0.0.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
+ CEF7F6AD2AEEDCC400E34952 /* glib-2.0.0.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE2D640422653C7500FC7E63 /* glib-2.0.0.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
+ CEF7F6AE2AEEDCC400E34952 /* EGL.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE5451A426AF5F0F008594E5 /* EGL.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+ CEF7F6B42AEEDCC400E34952 /* intl.8.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE2D63DA22653C7300FC7E63 /* intl.8.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
+ CEF7F6B52AEEDCC400E34952 /* gstreamer-1.0.0.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE2D63E022653C7400FC7E63 /* gstreamer-1.0.0.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
+ CEF7F6B62AEEDCC400E34952 /* GLESv2.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE5451A326AF5F0F008594E5 /* GLESv2.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+ CEF7F6B72AEEDCC400E34952 /* gstvideo-1.0.0.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE2D63F922653C7400FC7E63 /* gstvideo-1.0.0.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
+ CEF7F6B82AEEDCC400E34952 /* json-glib-1.0.0.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE2D63E222653C7400FC7E63 /* json-glib-1.0.0.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
+ CEF7F6B92AEEDCC400E34952 /* pixman-1.0.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE2D641922653C7600FC7E63 /* pixman-1.0.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
+ CEF7F6BA2AEEDCC400E34952 /* jpeg.62.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE2D63D922653C7300FC7E63 /* jpeg.62.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
+ CEF7F6BE2AEEDCC400E34952 /* spice-client-glib-2.0.8.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE2D63FE22653C7500FC7E63 /* spice-client-glib-2.0.8.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
+ CEF7F6BF2AEEDCC400E34952 /* opus.0.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE2D640322653C7500FC7E63 /* opus.0.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
+ CEF7F6C02AEEDCC400E34952 /* gstsdp-1.0.0.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE2D641622653C7500FC7E63 /* gstsdp-1.0.0.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
+ CEF7F6C42AEEDCC400E34952 /* gstaudio-1.0.0.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE2D63EF22653C7400FC7E63 /* gstaudio-1.0.0.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
+ CEF7F6C52AEEDCC400E34952 /* gstcheck-1.0.0.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE2D641422653C7500FC7E63 /* gstcheck-1.0.0.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
+ CEF7F6C72AEEDCC400E34952 /* iconv.2.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE2D641522653C7500FC7E63 /* iconv.2.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
+ CEF7F6C92AEEDCC400E34952 /* gstrtsp-1.0.0.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE2D640122653C7500FC7E63 /* gstrtsp-1.0.0.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
+ CEF7F6CB2AEEDCC400E34952 /* gcrypt.20.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE2D63F322653C7400FC7E63 /* gcrypt.20.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
+ CEF7F6CC2AEEDCC400E34952 /* gstfft-1.0.0.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE2D640922653C7500FC7E63 /* gstfft-1.0.0.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
+ CEF7F6CE2AEEDCC400E34952 /* gstpbutils-1.0.0.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE2D640E22653C7500FC7E63 /* gstpbutils-1.0.0.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
+ CEF7F6D62AEEEF7D00E34952 /* CocoaSpiceNoUsb in Frameworks */ = {isa = PBXBuildFile; productRef = CEF7F6D52AEEEF7D00E34952 /* CocoaSpiceNoUsb */; };
CEF83F262500901300557D15 /* qemu in Resources */ = {isa = PBXBuildFile; fileRef = CE9D18F72265410E00355E14 /* qemu */; };
CEF83F862500947D00557D15 /* gcrypt.20.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE2D63F322653C7400FC7E63 /* gcrypt.20.framework */; };
CEF83F872500948800557D15 /* gpg-error.0.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE2D63F122653C7400FC7E63 /* gpg-error.0.framework */; };
@@ -924,6 +1202,7 @@
CEF83F8D250094E700557D15 /* gthread-2.0.0.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE2D63DC22653C7300FC7E63 /* gthread-2.0.0.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
CEF83F8E250094EC00557D15 /* gpg-error.0.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE2D63F122653C7400FC7E63 /* gpg-error.0.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
CEF83F8F250094EE00557D15 /* gcrypt.20.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE2D63F322653C7400FC7E63 /* gcrypt.20.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
+ CEFE96772B69A7CC000F00C9 /* VMRemoteSessionState.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEFE96762B69A7CC000F00C9 /* VMRemoteSessionState.swift */; };
CEFE98DF29485237007CB7A8 /* UTM.sdef in Resources */ = {isa = PBXBuildFile; fileRef = CEFE98DE29485237007CB7A8 /* UTM.sdef */; };
CEFE98E129485776007CB7A8 /* UTMScriptingVirtualMachineImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEFE98E029485776007CB7A8 /* UTMScriptingVirtualMachineImpl.swift */; };
FF0307552A84E3B70049979B /* QEMULauncher-InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = FF0307532A84E3B70049979B /* QEMULauncher-InfoPlist.strings */; };
@@ -1243,9 +1522,63 @@
name = "Embed XPC Services";
runOnlyForDeploymentPostprocessing = 0;
};
+ CEF7F6822AEEDCC400E34952 /* Embed Libraries */ = {
+ isa = PBXCopyFilesBuildPhase;
+ buildActionMask = 2147483647;
+ dstPath = "";
+ dstSubfolderSpec = 10;
+ files = (
+ CEF7F6832AEEDCC400E34952 /* gpg-error.0.framework in Embed Libraries */,
+ CEF7F6852AEEDCC400E34952 /* gstcontroller-1.0.0.framework in Embed Libraries */,
+ CEF7F6862AEEDCC400E34952 /* gstallocators-1.0.0.framework in Embed Libraries */,
+ CEF7F6872AEEDCC400E34952 /* gstbase-1.0.0.framework in Embed Libraries */,
+ CEF7F6882AEEDCC400E34952 /* ffi.7.framework in Embed Libraries */,
+ CEF7F6892AEEDCC400E34952 /* ssl.1.1.framework in Embed Libraries */,
+ CEF7F68B2AEEDCC400E34952 /* gio-2.0.0.framework in Embed Libraries */,
+ CEF7F68D2AEEDCC400E34952 /* png16.16.framework in Embed Libraries */,
+ CEF7F68E2AEEDCC400E34952 /* gstnet-1.0.0.framework in Embed Libraries */,
+ CEF7F6902AEEDCC400E34952 /* crypto.1.1.framework in Embed Libraries */,
+ CEF7F6922AEEDCC400E34952 /* gstapp-1.0.0.framework in Embed Libraries */,
+ CEF7F6962AEEDCC400E34952 /* gsttag-1.0.0.framework in Embed Libraries */,
+ CEF7F6982AEEDCC400E34952 /* gstrtp-1.0.0.framework in Embed Libraries */,
+ CEF7F6992AEEDCC400E34952 /* gstriff-1.0.0.framework in Embed Libraries */,
+ CEF7F69C2AEEDCC400E34952 /* phodav-2.0.0.framework in Embed Libraries */,
+ CEF7F69D2AEEDCC400E34952 /* gthread-2.0.0.framework in Embed Libraries */,
+ CEF7F6A22AEEDCC400E34952 /* gobject-2.0.0.framework in Embed Libraries */,
+ CEF7F6A32AEEDCC400E34952 /* gmodule-2.0.0.framework in Embed Libraries */,
+ CEF7F6AD2AEEDCC400E34952 /* glib-2.0.0.framework in Embed Libraries */,
+ CEF7F6AE2AEEDCC400E34952 /* EGL.framework in Embed Libraries */,
+ CEF7F6B42AEEDCC400E34952 /* intl.8.framework in Embed Libraries */,
+ CEF7F6B52AEEDCC400E34952 /* gstreamer-1.0.0.framework in Embed Libraries */,
+ CEF7F6B62AEEDCC400E34952 /* GLESv2.framework in Embed Libraries */,
+ CEF7F6B72AEEDCC400E34952 /* gstvideo-1.0.0.framework in Embed Libraries */,
+ CEF7F6B82AEEDCC400E34952 /* json-glib-1.0.0.framework in Embed Libraries */,
+ CEF7F6B92AEEDCC400E34952 /* pixman-1.0.framework in Embed Libraries */,
+ CEF7F6BA2AEEDCC400E34952 /* jpeg.62.framework in Embed Libraries */,
+ CEF7F6BE2AEEDCC400E34952 /* spice-client-glib-2.0.8.framework in Embed Libraries */,
+ CEF7F6BF2AEEDCC400E34952 /* opus.0.framework in Embed Libraries */,
+ CEF7F6C02AEEDCC400E34952 /* gstsdp-1.0.0.framework in Embed Libraries */,
+ CEF7F6C42AEEDCC400E34952 /* gstaudio-1.0.0.framework in Embed Libraries */,
+ CEF7F6C52AEEDCC400E34952 /* gstcheck-1.0.0.framework in Embed Libraries */,
+ CEF7F6C72AEEDCC400E34952 /* iconv.2.framework in Embed Libraries */,
+ CEF7F6C92AEEDCC400E34952 /* gstrtsp-1.0.0.framework in Embed Libraries */,
+ CEF7F6CB2AEEDCC400E34952 /* gcrypt.20.framework in Embed Libraries */,
+ CEF7F6CC2AEEDCC400E34952 /* gstfft-1.0.0.framework in Embed Libraries */,
+ CEF7F6CE2AEEDCC400E34952 /* gstpbutils-1.0.0.framework in Embed Libraries */,
+ );
+ name = "Embed Libraries";
+ runOnlyForDeploymentPostprocessing = 0;
+ };
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
+ 037DAA1C2B0B92580061ACB3 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/VMDisplayWindow.strings; sourceTree = ""; };
+ 037DAA1D2B0B92580061ACB3 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/InfoPlist.strings; sourceTree = ""; };
+ 037DAA1E2B0B92580061ACB3 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/InfoPlist.strings; sourceTree = ""; };
+ 037DAA1F2B0B92580061ACB3 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/Localizable.strings; sourceTree = ""; };
+ 037DAA202B0B92580061ACB3 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = it; path = it.lproj/Localizable.stringsdict; sourceTree = ""; };
+ 037DAA212B0B92580061ACB3 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/Localizable.strings; sourceTree = ""; };
+ 037DAA222B0B92580061ACB3 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/InfoPlist.strings; sourceTree = ""; };
2C33B3A82566C9B100A954A6 /* VMContextMenuModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VMContextMenuModifier.swift; sourceTree = ""; };
2C6D9E02256EE454003298E6 /* VMDisplayQemuTerminalWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VMDisplayQemuTerminalWindowController.swift; sourceTree = ""; };
4B224B9C279D4D8100B63CFF /* InListButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InListButtonStyle.swift; sourceTree = ""; };
@@ -1437,10 +1770,12 @@
CE061CE8289EB6250000351C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/VMDisplayMetalViewInputAccessory.xib; sourceTree = ""; };
CE061CEB289EB62E0000351C /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/VMDisplayMetalViewInputAccessory.strings; sourceTree = ""; };
CE064C642A563F4A003C833D /* swtpm.0.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = swtpm.0.framework; path = "$(SYSROOT_DIR)/Frameworks/swtpm.0.framework"; sourceTree = ""; };
+ CE08334A2B784FD400522C03 /* RemoteContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteContentView.swift; sourceTree = ""; };
CE0DF17025A80B6300A51894 /* Bootstrap.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Bootstrap.h; sourceTree = ""; };
CE0DF17125A80B6300A51894 /* Bootstrap.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = Bootstrap.c; sourceTree = ""; };
CE0E9B86252FD06B0026E02B /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; };
CE19392526DCB093005CEC17 /* RAMSlider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RAMSlider.swift; sourceTree = ""; };
+ CE1AEC3E2B78B30700992AFC /* MacDeviceLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MacDeviceLabel.swift; sourceTree = ""; };
CE20FAE62448D2BE0059AE11 /* VMScroll.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = VMScroll.h; sourceTree = ""; };
CE20FAE72448D2BE0059AE11 /* VMScroll.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = VMScroll.m; sourceTree = ""; };
CE25124629BFDB87000790AB /* UTMScriptingGuestProcessImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMScriptingGuestProcessImpl.swift; sourceTree = ""; };
@@ -1542,6 +1877,7 @@
CE2D955624AD4F980059923A /* Swift-Bridging-Header.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "Swift-Bridging-Header.h"; sourceTree = ""; };
CE31C243225E553500A965DD /* UTMLegacyQemuConfiguration.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = UTMLegacyQemuConfiguration.h; sourceTree = ""; };
CE31C244225E555600A965DD /* UTMLegacyQemuConfiguration.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = UTMLegacyQemuConfiguration.m; sourceTree = ""; };
+ CE38EC682B5DB3AE008B324B /* UTMRemoteClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMRemoteClient.swift; sourceTree = ""; };
CE3ADD65240EFBCA002D6A5F /* VMDisplayMetalViewController+Keyboard.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "VMDisplayMetalViewController+Keyboard.h"; sourceTree = ""; };
CE3ADD66240EFBCA002D6A5F /* VMDisplayMetalViewController+Keyboard.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "VMDisplayMetalViewController+Keyboard.m"; sourceTree = ""; };
CE3ADD682411C661002D6A5F /* VMCursor.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = VMCursor.h; sourceTree = ""; };
@@ -1572,11 +1908,13 @@
CE6B240A25F1F3CE0020D43E /* main.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = main.c; sourceTree = ""; };
CE6B240F25F1F43A0020D43E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
CE6B241025F1F4B30020D43E /* QEMULauncher.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = QEMULauncher.entitlements; sourceTree = ""; };
+ CE6C13C92B63610C003B7032 /* UTMRemoteMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMRemoteMessage.swift; sourceTree = ""; };
CE6D21DB2553A6ED001D29C5 /* VMConfirmActionModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VMConfirmActionModifier.swift; sourceTree = ""; };
CE6EDCDD241C4A6800A719DC /* UTMLegacyViewState.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = UTMLegacyViewState.h; sourceTree = ""; };
CE6EDCDE241C4A6800A719DC /* UTMLegacyViewState.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = UTMLegacyViewState.m; sourceTree = ""; };
CE6EDCE0241DA0E900A719DC /* UTMLogging.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = UTMLogging.h; sourceTree = ""; };
CE6EDCE1241DA0E900A719DC /* UTMLogging.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = UTMLogging.m; sourceTree = ""; };
+ CE70E8D42B648FBE007FA787 /* UTMRemoteSpiceVirtualMachine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMRemoteSpiceVirtualMachine.swift; sourceTree = ""; };
CE72B4AB2463579D00716A11 /* VMDisplayViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = VMDisplayViewController.h; sourceTree = ""; };
CE72B4AC2463579D00716A11 /* VMDisplayViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = VMDisplayViewController.m; sourceTree = ""; };
CE772AAB25C8B0F600E4E379 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; };
@@ -1592,6 +1930,10 @@
CE9A352D26533A51005077CF /* JailbreakInterposer.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = JailbreakInterposer.framework; sourceTree = BUILT_PRODUCTS_DIR; };
CE9A353026533A52005077CF /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
CE9A353F26533AE6005077CF /* JailbreakInterposer.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = JailbreakInterposer.c; sourceTree = ""; };
+ CE9B153E2B11A63E003A32DD /* UTMRemoteServer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMRemoteServer.swift; sourceTree = ""; };
+ CE9B15402B11A74E003A32DD /* UTMRemoteKeyManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMRemoteKeyManager.swift; sourceTree = ""; };
+ CE9B15452B12A87E003A32DD /* GenerateKey.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GenerateKey.h; sourceTree = ""; };
+ CE9B15462B12A87E003A32DD /* GenerateKey.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = GenerateKey.c; sourceTree = ""; };
CE9D18F72265410E00355E14 /* qemu */ = {isa = PBXFileReference; lastKnownFileType = folder; name = qemu; path = "$(SYSROOT_DIR)/share/qemu"; sourceTree = ""; };
CE9D19522265425900355E14 /* libgstautodetect.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libgstautodetect.a; path = "$(SYSROOT_DIR)/lib/gstreamer-1.0/libgstautodetect.a"; sourceTree = ""; };
CE9D19532265425900355E14 /* libgstaudiotestsrc.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libgstaudiotestsrc.a; path = "$(SYSROOT_DIR)/lib/gstreamer-1.0/libgstaudiotestsrc.a"; sourceTree = ""; };
@@ -1630,6 +1972,8 @@
CEB54C1829300C1B000D2AA9 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/Localizable.strings; sourceTree = ""; };
CEB54C1929300C20000D2AA9 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/InfoPlist.strings; sourceTree = ""; };
CEB54C802931C43F000D2AA9 /* UTMPatches.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMPatches.swift; sourceTree = ""; };
+ CEB5C1182B8C4CD4008AAE5C /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = "en.lproj/Info-RemotePlist.strings"; sourceTree = ""; };
+ CEB5C11A2B8C4D30008AAE5C /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = "ja.lproj/Info-RemotePlist.strings"; sourceTree = ""; };
CEB63A7524F4654400CAF323 /* Main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Main.swift; sourceTree = ""; };
CEB63A7824F468BA00CAF323 /* UTMJailbreak.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = UTMJailbreak.h; sourceTree = ""; };
CEB63A7924F469E300CAF323 /* UTMJailbreak.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = UTMJailbreak.m; sourceTree = ""; };
@@ -1649,6 +1993,8 @@
CEC794B9294924E300121A9F /* UTMScriptingSerialPortImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMScriptingSerialPortImpl.swift; sourceTree = ""; };
CEC794BB2949663C00121A9F /* UTMScripting.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UTMScripting.swift; sourceTree = ""; };
CEC9968328AA516000E7A025 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ja; path = ja.lproj/Localizable.stringsdict; sourceTree = ""; };
+ CECF02562B706ADD00409FC0 /* UTMRemoteConnectInterface.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = UTMRemoteConnectInterface.h; sourceTree = ""; };
+ CECF02572B70909900409FC0 /* Info-Remote.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "Info-Remote.plist"; sourceTree = ""; };
CED234EC254796E500ED0A57 /* NumberTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NumberTextField.swift; sourceTree = ""; };
CED814E824C79F070042F0F1 /* VMConfigDriveCreateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VMConfigDriveCreateView.swift; sourceTree = ""; };
CED814EB24C7C2850042F0F1 /* VMConfigInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VMConfigInfoView.swift; sourceTree = ""; };
@@ -1662,13 +2008,18 @@
CEE0420B244117040001680F /* UTMLegacyQemuConfiguration+Display.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UTMLegacyQemuConfiguration+Display.m"; sourceTree = ""; };
CEE0421024418F2E0001680F /* UTMLegacyQemuConfiguration+Miscellaneous.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UTMLegacyQemuConfiguration+Miscellaneous.h"; sourceTree = ""; };
CEE0421124418F2E0001680F /* UTMLegacyQemuConfiguration+Miscellaneous.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UTMLegacyQemuConfiguration+Miscellaneous.m"; sourceTree = ""; };
+ CEE06B262B2FC89400A811AE /* UTMServerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMServerView.swift; sourceTree = ""; };
+ CEE06B282B30013500A811AE /* UTMRemoteConnectView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMRemoteConnectView.swift; sourceTree = ""; };
CEE7E934287CFDB100282049 /* UTMLegacyQemuConfiguration+Constants.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UTMLegacyQemuConfiguration+Constants.m"; sourceTree = ""; };
CEE7E935287CFDB100282049 /* UTMLegacyQemuConfiguration+Constants.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UTMLegacyQemuConfiguration+Constants.h"; sourceTree = ""; };
CEE7ED472A90256100E6B4AB /* VMDisplayMetalViewController+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "VMDisplayMetalViewController+Private.h"; sourceTree = ""; };
+ CEE8B4C12B71DF4C0035AE86 /* UTMQemuSystemBackends.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = UTMQemuSystemBackends.h; sourceTree = ""; };
CEEB66442284B942002737B2 /* VMKeyboardButton.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = VMKeyboardButton.h; sourceTree = ""; };
CEEB66452284B942002737B2 /* VMKeyboardButton.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = VMKeyboardButton.m; sourceTree = ""; };
CEEC811A24E48EC600ACB0B3 /* SettingsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; };
CEECE13B25E47D9500A2AAB8 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
+ CEF01DB12B6724A300725A0F /* UTMSpiceVirtualMachine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMSpiceVirtualMachine.swift; sourceTree = ""; };
+ CEF01DB62B674BF000725A0F /* UTMPipeInterface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMPipeInterface.swift; sourceTree = ""; };
CEF0300526A25A6900667B63 /* VMWizardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VMWizardView.swift; sourceTree = ""; };
CEF0304C26A2AFBE00667B63 /* BigButtonStyle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BigButtonStyle.swift; sourceTree = ""; };
CEF0304D26A2AFBE00667B63 /* Spinner.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Spinner.swift; sourceTree = ""; };
@@ -1684,7 +2035,9 @@
CEF6F5EA26DDD60500BC434D /* macOS-unsigned.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "macOS-unsigned.entitlements"; sourceTree = ""; };
CEF6F5EB26DDD63100BC434D /* QEMUHelper-unsigned.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "QEMUHelper-unsigned.entitlements"; sourceTree = ""; };
CEF6F5EC26DDD65700BC434D /* QEMULauncher-unsigned.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "QEMULauncher-unsigned.entitlements"; sourceTree = ""; };
+ CEF7F6D32AEEDCC400E34952 /* UTM Remote.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "UTM Remote.app"; sourceTree = BUILT_PRODUCTS_DIR; };
CEF84ADA2887D7D300578F41 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Localizable.strings; sourceTree = ""; };
+ CEFE96762B69A7CC000F00C9 /* VMRemoteSessionState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VMRemoteSessionState.swift; sourceTree = ""; };
CEFE98DE29485237007CB7A8 /* UTM.sdef */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = UTM.sdef; sourceTree = ""; };
CEFE98E029485776007CB7A8 /* UTMScriptingVirtualMachineImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMScriptingVirtualMachineImpl.swift; sourceTree = ""; };
E2D64BC7241DB24B0034E0C6 /* UTMSpiceIO.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = UTMSpiceIO.h; sourceTree = ""; };
@@ -1780,6 +2133,7 @@
84818C0C2898A07A009EDB67 /* AVFAudio.framework in Frameworks */,
CE2D934924AD46670059923A /* gstreamer-1.0.0.framework in Frameworks */,
CE2D934B24AD46670059923A /* json-glib-1.0.0.framework in Frameworks */,
+ CE89CB0E2B8B1B5A006B2CC2 /* VisionKeyboardKit in Frameworks */,
CE2D934C24AD46670059923A /* ffi.7.framework in Frameworks */,
CE2D934D24AD46670059923A /* gstnet-1.0.0.framework in Frameworks */,
CE2D934E24AD46670059923A /* gstbase-1.0.0.framework in Frameworks */,
@@ -1812,6 +2166,7 @@
CE2D936324AD46670059923A /* iconv.2.framework in Frameworks */,
CE2D936424AD46670059923A /* gstsdp-1.0.0.framework in Frameworks */,
84B36D1E27B3264600C22685 /* CocoaSpice in Frameworks */,
+ CE9B15382B11A4A7003A32DD /* SwiftConnect in Frameworks */,
CE2D936524AD46670059923A /* ssl.1.1.framework in Frameworks */,
CE2D936624AD46670059923A /* spice-server.1.framework in Frameworks */,
CE2D936724AD46670059923A /* pixman-1.0.framework in Frameworks */,
@@ -1851,6 +2206,7 @@
CE03D0CE24D9A30100F76B84 /* iconv.2.framework in Frameworks */,
CE0B6EF124AD677200FE012D /* libgstplayback.a in Frameworks */,
CE0B6EF424AD677200FE012D /* json-glib-1.0.0.framework in Frameworks */,
+ CEDD11C12B7C74D7004DDAC6 /* SwiftPortmap in Frameworks */,
CE0B6ED124AD677200FE012D /* phodav-2.0.0.framework in Frameworks */,
CEF83F862500947D00557D15 /* gcrypt.20.framework in Frameworks */,
CE0B6ECB24AD677200FE012D /* gstcheck-1.0.0.framework in Frameworks */,
@@ -1860,6 +2216,7 @@
CE0B6EE524AD677200FE012D /* gstbase-1.0.0.framework in Frameworks */,
CEF83F882500949D00557D15 /* gthread-2.0.0.framework in Frameworks */,
CE03D08724D90F0700F76B84 /* gobject-2.0.0.framework in Frameworks */,
+ CE9B15362B11A491003A32DD /* SwiftConnect in Frameworks */,
CE0B6F0A24AD677200FE012D /* spice-client-glib-2.0.8.framework in Frameworks */,
CE0B6ECF24AD677200FE012D /* gstrtp-1.0.0.framework in Frameworks */,
CE0B6ECC24AD677200FE012D /* gstriff-1.0.0.framework in Frameworks */,
@@ -1907,6 +2264,7 @@
CEA45F25263519B5002FA97D /* libgstaudiotestsrc.a in Frameworks */,
CEA45F26263519B5002FA97D /* libgstvideoconvert.a in Frameworks */,
CEA45F27263519B5002FA97D /* libgstaudioconvert.a in Frameworks */,
+ CE89CB102B8B1B6A006B2CC2 /* VisionKeyboardKit in Frameworks */,
8401865C2887AFDC0050AC51 /* SwiftTerm in Frameworks */,
CEA45F28263519B5002FA97D /* libgstvideoscale.a in Frameworks */,
CEA45F29263519B5002FA97D /* IQKeyboardManagerSwift in Frameworks */,
@@ -1919,6 +2277,7 @@
CEA45F2E263519B5002FA97D /* libgstjpeg.a in Frameworks */,
CEA45F2F263519B5002FA97D /* libgstaudioresample.a in Frameworks */,
CEA45F30263519B5002FA97D /* libgstplayback.a in Frameworks */,
+ CE9B153A2B11A4AE003A32DD /* SwiftConnect in Frameworks */,
CEA45F31263519B5002FA97D /* libgstadder.a in Frameworks */,
CE02C8B1294EE58C006DFE48 /* slirp.0.framework in Frameworks */,
CEA45F32263519B5002FA97D /* libgstaudiorate.a in Frameworks */,
@@ -1981,6 +2340,79 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
+ CEF7F62D2AEEDCC400E34952 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ CEF7F62E2AEEDCC400E34952 /* libgstautodetect.a in Frameworks */,
+ CEF7F62F2AEEDCC400E34952 /* libgstaudiotestsrc.a in Frameworks */,
+ CEF7F6302AEEDCC400E34952 /* libgstvideoconvert.a in Frameworks */,
+ CEF7F6312AEEDCC400E34952 /* libgstaudioconvert.a in Frameworks */,
+ CEF7F6322AEEDCC400E34952 /* libgstvideoscale.a in Frameworks */,
+ CEF7F6332AEEDCC400E34952 /* IQKeyboardManagerSwift in Frameworks */,
+ CEF7F6342AEEDCC400E34952 /* MetalKit.framework in Frameworks */,
+ CEF7F6352AEEDCC400E34952 /* libgstvolume.a in Frameworks */,
+ CEF7F6362AEEDCC400E34952 /* libgstcoreelements.a in Frameworks */,
+ CEF7F6372AEEDCC400E34952 /* libgstvideorate.a in Frameworks */,
+ CEF7F6382AEEDCC400E34952 /* libgstjpeg.a in Frameworks */,
+ CEF7F6392AEEDCC400E34952 /* libgstaudioresample.a in Frameworks */,
+ CEF7F63A2AEEDCC400E34952 /* libgstplayback.a in Frameworks */,
+ CEF7F63C2AEEDCC400E34952 /* libgstadder.a in Frameworks */,
+ CEF7F63D2AEEDCC400E34952 /* libgstaudiorate.a in Frameworks */,
+ CEF7F63F2AEEDCC400E34952 /* libgstvideofilter.a in Frameworks */,
+ CEF7F6402AEEDCC400E34952 /* SwiftUIVisualEffects in Frameworks */,
+ CEF7F6422AEEDCC400E34952 /* libgstapp.a in Frameworks */,
+ CEF7F6432AEEDCC400E34952 /* libgstgio.a in Frameworks */,
+ CEF7F6442AEEDCC400E34952 /* libgsttypefindfunctions.a in Frameworks */,
+ CEF7F6452AEEDCC400E34952 /* libgstvideotestsrc.a in Frameworks */,
+ CEF7F6462AEEDCC400E34952 /* libgstosxaudio.a in Frameworks */,
+ CEF7F6472AEEDCC400E34952 /* gmodule-2.0.0.framework in Frameworks */,
+ CEF7F6482AEEDCC400E34952 /* jpeg.62.framework in Frameworks */,
+ CE9B153C2B11A4B4003A32DD /* SwiftConnect in Frameworks */,
+ CEF7F6492AEEDCC400E34952 /* ZIPFoundation in Frameworks */,
+ CEF7F64A2AEEDCC400E34952 /* intl.8.framework in Frameworks */,
+ CEF7F64B2AEEDCC400E34952 /* gstapp-1.0.0.framework in Frameworks */,
+ CEF7F64C2AEEDCC400E34952 /* gthread-2.0.0.framework in Frameworks */,
+ CEF7F64D2AEEDCC400E34952 /* gstrtp-1.0.0.framework in Frameworks */,
+ CEF7F64E2AEEDCC400E34952 /* gstriff-1.0.0.framework in Frameworks */,
+ CEF7F6512AEEDCC400E34952 /* AVFAudio.framework in Frameworks */,
+ CEF7F6522AEEDCC400E34952 /* gstreamer-1.0.0.framework in Frameworks */,
+ CEF7F6532AEEDCC400E34952 /* json-glib-1.0.0.framework in Frameworks */,
+ CEF7F6542AEEDCC400E34952 /* ffi.7.framework in Frameworks */,
+ CEF7F6552AEEDCC400E34952 /* gstnet-1.0.0.framework in Frameworks */,
+ CEF7F6562AEEDCC400E34952 /* gstbase-1.0.0.framework in Frameworks */,
+ CEF7F6572AEEDCC400E34952 /* Logging in Frameworks */,
+ CEF7F6582AEEDCC400E34952 /* SwiftTerm in Frameworks */,
+ CEF7F65A2AEEDCC400E34952 /* phodav-2.0.0.framework in Frameworks */,
+ CEF7F65C2AEEDCC400E34952 /* SwiftUI.framework in Frameworks */,
+ CEF7F65D2AEEDCC400E34952 /* gstcontroller-1.0.0.framework in Frameworks */,
+ CEF7F65E2AEEDCC400E34952 /* gstaudio-1.0.0.framework in Frameworks */,
+ CEF7F65F2AEEDCC400E34952 /* gpg-error.0.framework in Frameworks */,
+ CEF7F6602AEEDCC400E34952 /* gcrypt.20.framework in Frameworks */,
+ CEF7F6612AEEDCC400E34952 /* InAppSettingsKit in Frameworks */,
+ CEF7F6622AEEDCC400E34952 /* gobject-2.0.0.framework in Frameworks */,
+ CEF7F6642AEEDCC400E34952 /* gsttag-1.0.0.framework in Frameworks */,
+ CEF7F6652AEEDCC400E34952 /* gio-2.0.0.framework in Frameworks */,
+ CEF7F6662AEEDCC400E34952 /* gstvideo-1.0.0.framework in Frameworks */,
+ CEF7F6672AEEDCC400E34952 /* spice-client-glib-2.0.8.framework in Frameworks */,
+ CEF7F6D62AEEEF7D00E34952 /* CocoaSpiceNoUsb in Frameworks */,
+ CEF7F6682AEEDCC400E34952 /* gstrtsp-1.0.0.framework in Frameworks */,
+ CEF7F6692AEEDCC400E34952 /* opus.0.framework in Frameworks */,
+ CEF7F66A2AEEDCC400E34952 /* glib-2.0.0.framework in Frameworks */,
+ CEF7F66B2AEEDCC400E34952 /* png16.16.framework in Frameworks */,
+ CE89CB122B8B1B7A006B2CC2 /* VisionKeyboardKit in Frameworks */,
+ CEF7F66C2AEEDCC400E34952 /* gstfft-1.0.0.framework in Frameworks */,
+ CEF7F66D2AEEDCC400E34952 /* crypto.1.1.framework in Frameworks */,
+ CEF7F66E2AEEDCC400E34952 /* gstpbutils-1.0.0.framework in Frameworks */,
+ CEF7F66F2AEEDCC400E34952 /* gstallocators-1.0.0.framework in Frameworks */,
+ CEF7F6702AEEDCC400E34952 /* gstcheck-1.0.0.framework in Frameworks */,
+ CEF7F6712AEEDCC400E34952 /* iconv.2.framework in Frameworks */,
+ CEF7F6722AEEDCC400E34952 /* gstsdp-1.0.0.framework in Frameworks */,
+ CEF7F6742AEEDCC400E34952 /* ssl.1.1.framework in Frameworks */,
+ CEF7F6762AEEDCC400E34952 /* pixman-1.0.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
@@ -2191,6 +2623,7 @@
CEBBF1A624B5730F00C15049 /* UTMDataExtension.swift */,
84E3A91A2946D2590024A740 /* UTMMenuBarExtraScene.swift */,
CEB54C802931C43F000D2AA9 /* UTMPatches.swift */,
+ CEE06B262B2FC89400A811AE /* UTMServerView.swift */,
8401FD9F269D266E00265F0D /* VMConfigAppleBootView.swift */,
8401FDA3269D43CF00265F0D /* VMConfigAppleDisplayView.swift */,
8401FDAF269E1F7F00265F0D /* VMConfigAppleDriveCreateView.swift */,
@@ -2210,6 +2643,7 @@
CE2D953D24AD4F980059923A /* VMSettingsView.swift */,
CEF0300526A25A6900667B63 /* VMWizardView.swift */,
84BB99392899E8D500DF28B2 /* VMHeadlessSessionState.swift */,
+ CEFE96762B69A7CC000F00C9 /* VMRemoteSessionState.swift */,
53A0BDD426D79FE40010EDC5 /* SavePanel.swift */,
CE2D954124AD4F980059923A /* Info.plist */,
FFB02A8E266CB09C006CD71A /* InfoPlist.strings */,
@@ -2227,11 +2661,13 @@
CE8813D224CD230300532628 /* ActivityView.swift */,
CED814EE24C7EB760042F0F1 /* ImagePicker.swift */,
84CE3DAD2904C17C00FF068B /* IASKAppSettings.swift */,
+ CE08334A2B784FD400522C03 /* RemoteContentView.swift */,
841E58D02893AF5400137A20 /* UTMApp.swift */,
CEBBF1A424B56A2900C15049 /* UTMDataExtension.swift */,
841E58CA28937EE200137A20 /* UTMExternalSceneDelegate.swift */,
841E58CD28937FED00137A20 /* UTMSingleWindowView.swift */,
842B9F8C28CC58B700031EE7 /* UTMPatches.swift */,
+ CEE06B282B30013500A811AE /* UTMRemoteConnectView.swift */,
84CE3DB02904C7A100FF068B /* UTMSettingsView.swift */,
CE2D954D24AD4F980059923A /* VMConfigNetworkPortForwardView.swift */,
84CF5DD2288DCE6400D01721 /* VMDisplayHostedView.swift */,
@@ -2247,7 +2683,9 @@
CEF0307026A2B04300667B63 /* VMWizardView.swift */,
CE95877426D74C2A0086BDE8 /* iOS.entitlements */,
CE2D954F24AD4F980059923A /* Info.plist */,
+ CECF02572B70909900409FC0 /* Info-Remote.plist */,
FFB02A8A266CB09C006CD71A /* InfoPlist.strings */,
+ CEB5C1192B8C4CD4008AAE5C /* Info-RemotePlist.strings */,
5286EC91243748AC007E6CBC /* Settings.bundle */,
);
path = iOS;
@@ -2305,6 +2743,7 @@
CE9D18F72265410E00355E14 /* qemu */,
CE6B240925F1F3CE0020D43E /* QEMULauncher */,
CE9A352E26533A51005077CF /* JailbreakInterposer */,
+ CE9B153D2B11A4ED003A32DD /* Remote */,
CE4698F824C8FBD9008C1BD6 /* Icons */,
CEFE98DD2948518D007CB7A8 /* Scripting */,
84E3A8F1293DB37E0024A740 /* utmctl */,
@@ -2324,6 +2763,7 @@
CE9A352D26533A51005077CF /* JailbreakInterposer.framework */,
8401FD62269BE9C500265F0D /* QEMULauncher.app */,
84E3A8F0293DB37E0024A740 /* utmctl */,
+ CEF7F6D32AEEDCC400E34952 /* UTM Remote.app */,
);
name = Products;
sourceTree = "";
@@ -2341,12 +2781,14 @@
CE6EDCE1241DA0E900A719DC /* UTMLogging.m */,
CE020BAA24AEE00000B44AB6 /* UTMLoggingSwift.swift */,
CEDF83F8258AE24E0030E4AC /* UTMPasteboard.swift */,
+ CEF01DB62B674BF000725A0F /* UTMPipeInterface.swift */,
CE9D197A226542FE00355E14 /* UTMProcess.h */,
CE9D197B226542FE00355E14 /* UTMProcess.m */,
8453DCB3278CE5410037A0DA /* UTMQemuImage.swift */,
84A0A8822A47D52E0038F329 /* UTMQemuPort.swift */,
CE03D05424D90BE000F76B84 /* UTMQemuSystem.h */,
CE03D05024D90B4E00F76B84 /* UTMQemuSystem.m */,
+ CEE8B4C12B71DF4C0035AE86 /* UTMQemuSystemBackends.h */,
841E997428AA1191003C6CB6 /* UTMRegistry.swift */,
841E997828AA119B003C6CB6 /* UTMRegistryEntry.swift */,
848F71E7277A2A4E006A0240 /* UTMSerialPort.swift */,
@@ -2358,6 +2800,7 @@
CE020BB524B14F8400B44AB6 /* UTMVirtualMachine.swift */,
CE928C2926ABE6690099F293 /* UTMAppleVirtualMachine.swift */,
841E999728AC817D003C6CB6 /* UTMQemuVirtualMachine.swift */,
+ CEF01DB12B6724A300725A0F /* UTMSpiceVirtualMachine.swift */,
);
path = Services;
sourceTree = "";
@@ -2420,6 +2863,21 @@
path = JailbreakInterposer;
sourceTree = "";
};
+ CE9B153D2B11A4ED003A32DD /* Remote */ = {
+ isa = PBXGroup;
+ children = (
+ CE9B15402B11A74E003A32DD /* UTMRemoteKeyManager.swift */,
+ CE38EC682B5DB3AE008B324B /* UTMRemoteClient.swift */,
+ CE6C13C92B63610C003B7032 /* UTMRemoteMessage.swift */,
+ CE70E8D42B648FBE007FA787 /* UTMRemoteSpiceVirtualMachine.swift */,
+ CE9B153E2B11A63E003A32DD /* UTMRemoteServer.swift */,
+ CE9B15452B12A87E003A32DD /* GenerateKey.h */,
+ CE9B15462B12A87E003A32DD /* GenerateKey.c */,
+ CECF02562B706ADD00409FC0 /* UTMRemoteConnectInterface.h */,
+ );
+ path = Remote;
+ sourceTree = "";
+ };
CEB63A9624F47C1200CAF323 /* Shared */ = {
isa = PBXGroup;
children = (
@@ -2434,6 +2892,7 @@
CE772AAB25C8B0F600E4E379 /* ContentView.swift */,
8471770527CC974F00D3A50B /* DefaultTextField.swift */,
8432329328C2ED9000CFBC97 /* FileBrowseField.swift */,
+ CE1AEC3E2B78B30700992AFC /* MacDeviceLabel.swift */,
84F909FE289488F90008DBE2 /* MenuLabel.swift */,
CED234EC254796E500ED0A57 /* NumberTextField.swift */,
CE19392526DCB093005CEC17 /* RAMSlider.swift */,
@@ -2596,6 +3055,8 @@
84018694288B66370050AC51 /* SwiftUIVisualEffects */,
84CE3DAB2904C14100FF068B /* InAppSettingsKit */,
84A0A8892A47D5D10038F329 /* QEMUKit */,
+ CE9B15372B11A4A7003A32DD /* SwiftConnect */,
+ CE89CB0D2B8B1B5A006B2CC2 /* VisionKeyboardKit */,
);
productName = UTM;
productReference = CE2D93BE24AD46670059923A /* UTM.app */;
@@ -2627,6 +3088,8 @@
848F71E5277A2466006A0240 /* SwiftTerm */,
84B36D2127B3265400C22685 /* CocoaSpice */,
84A0A8872A47D5C50038F329 /* QEMUKit */,
+ CE9B15352B11A491003A32DD /* SwiftConnect */,
+ CEDD11C02B7C74D7004DDAC6 /* SwiftPortmap */,
);
productName = UTM;
productReference = CE2D951C24AD48BE0059923A /* UTM.app */;
@@ -2650,9 +3113,9 @@
productReference = CE9A352D26533A51005077CF /* JailbreakInterposer.framework */;
productType = "com.apple.product-type.framework";
};
- CEA45E1F263519B5002FA97D /* iOS-TCI */ = {
+ CEA45E1F263519B5002FA97D /* iOS-SE */ = {
isa = PBXNativeTarget;
- buildConfigurationList = CEA45FB6263519B5002FA97D /* Build configuration list for PBXNativeTarget "iOS-TCI" */;
+ buildConfigurationList = CEA45FB6263519B5002FA97D /* Build configuration list for PBXNativeTarget "iOS-SE" */;
buildPhases = (
CEA45E24263519B5002FA97D /* Sources */,
CEA45F23263519B5002FA97D /* Frameworks */,
@@ -2664,7 +3127,7 @@
);
dependencies = (
);
- name = "iOS-TCI";
+ name = "iOS-SE";
packageProductDependencies = (
CEA45E20263519B5002FA97D /* Logging */,
CEA45E22263519B5002FA97D /* IQKeyboardManagerSwift */,
@@ -2674,6 +3137,8 @@
84CF5DF2288E433F00D01721 /* SwiftUIVisualEffects */,
846D878529050B6B0095F10B /* InAppSettingsKit */,
84A0A88B2A47D5D70038F329 /* QEMUKit */,
+ CE9B15392B11A4AE003A32DD /* SwiftConnect */,
+ CE89CB0F2B8B1B6A006B2CC2 /* VisionKeyboardKit */,
);
productName = UTM;
productReference = CEA45FB9263519B5002FA97D /* UTM SE.app */;
@@ -2698,6 +3163,36 @@
productReference = CEBDA1DA24D8BDDA0010B5EC /* QEMUHelper.xpc */;
productType = "com.apple.product-type.xpc-service";
};
+ CEF7F5812AEEDCC400E34952 /* iOS-Remote */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = CEF7F6D02AEEDCC400E34952 /* Build configuration list for PBXNativeTarget "iOS-Remote" */;
+ buildPhases = (
+ CEF7F5962AEEDCC400E34952 /* Sources */,
+ CEF7F62D2AEEDCC400E34952 /* Frameworks */,
+ CEF7F6782AEEDCC400E34952 /* Resources */,
+ CEF7F6812AEEDCC400E34952 /* Patch Settings bundle */,
+ CEF7F6822AEEDCC400E34952 /* Embed Libraries */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ name = "iOS-Remote";
+ packageProductDependencies = (
+ CEF7F5842AEEDCC400E34952 /* Logging */,
+ CEF7F5862AEEDCC400E34952 /* IQKeyboardManagerSwift */,
+ CEF7F5882AEEDCC400E34952 /* ZIPFoundation */,
+ CEF7F58E2AEEDCC400E34952 /* SwiftTerm */,
+ CEF7F5902AEEDCC400E34952 /* SwiftUIVisualEffects */,
+ CEF7F5922AEEDCC400E34952 /* InAppSettingsKit */,
+ CEF7F6D52AEEEF7D00E34952 /* CocoaSpiceNoUsb */,
+ CE9B153B2B11A4B4003A32DD /* SwiftConnect */,
+ CE89CB112B8B1B7A006B2CC2 /* VisionKeyboardKit */,
+ );
+ productName = UTM;
+ productReference = CEF7F6D32AEEDCC400E34952 /* UTM Remote.app */;
+ productType = "com.apple.product-type.application";
+ };
/* End PBXNativeTarget section */
/* Begin PBXProject section */
@@ -2749,6 +3244,7 @@
pl,
"zh-HK",
ru,
+ it,
);
mainGroup = CE550BC0225947990063E575;
packageReferences = (
@@ -2762,13 +3258,17 @@
84CE3DAA2904C14100FF068B /* XCRemoteSwiftPackageReference "InAppSettingsKit" */,
84E3A8FE293DBC290024A740 /* XCRemoteSwiftPackageReference "swift-argument-parser" */,
84A0A8862A47D5C50038F329 /* XCRemoteSwiftPackageReference "QEMUKit" */,
+ CE9B15342B11A491003A32DD /* XCRemoteSwiftPackageReference "SwiftConnect" */,
+ CEDD11BF2B7C74D7004DDAC6 /* XCRemoteSwiftPackageReference "SwiftPortmap" */,
+ CE89CB0C2B8B1B49006B2CC2 /* XCRemoteSwiftPackageReference "VisionKeyboardKit" */,
);
productRefGroup = CE550BCA225947990063E575 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
CE2D926824AD46670059923A /* iOS */,
- CEA45E1F263519B5002FA97D /* iOS-TCI */,
+ CEA45E1F263519B5002FA97D /* iOS-SE */,
+ CEF7F5812AEEDCC400E34952 /* iOS-Remote */,
CE2D951B24AD48BE0059923A /* macOS */,
CEBDA1D924D8BDDA0010B5EC /* QEMUHelper */,
8401FD61269BE9C500265F0D /* QEMULauncher */,
@@ -2849,6 +3349,21 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
+ CEF7F6782AEEDCC400E34952 /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ CEF7F6792AEEDCC400E34952 /* Icons in Resources */,
+ CEB5C1172B8C4CD4008AAE5C /* Info-RemotePlist.strings in Resources */,
+ CEF7F67B2AEEDCC400E34952 /* Localizable.strings in Resources */,
+ CEF7F67C2AEEDCC400E34952 /* qemu in Resources */,
+ CEF7F67D2AEEDCC400E34952 /* VMDisplayMetalViewInputAccessory.xib in Resources */,
+ CEF7F67E2AEEDCC400E34952 /* Localizable.stringsdict in Resources */,
+ CEF7F67F2AEEDCC400E34952 /* Settings.bundle in Resources */,
+ CEF7F6802AEEDCC400E34952 /* Assets.xcassets in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
@@ -2870,7 +3385,7 @@
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
- shellScript = "#!/bin/sh\n\nPLISTBUDDY=/usr/libexec/PlistBuddy\nROOT_PLIST=\"$SCRIPT_OUTPUT_FILE_0\"\n\nCOUNT=$($PLISTBUDDY -c \"Print :PreferenceSpecifiers\" \"$ROOT_PLIST\" | grep -c '^ }$')\n\nfor ((i = 0; i < COUNT; i++)); do\n platform=$($PLISTBUDDY -c \"Print :PreferenceSpecifiers:$i:Platform\" \"$ROOT_PLIST\" 2> /dev/null)\n if [ ! -z \"$platform\" ]; then\n if [ \"$platform\" == \"$PLATFORM_FAMILY_NAME\" ]; then\n echo \"Found entry $i for $platform\"\n $PLISTBUDDY -c \"Delete :PreferenceSpecifiers:$i:Platform\" \"$ROOT_PLIST\"\n else\n echo \"Found entry $i for $platform, removing entry\"\n $PLISTBUDDY -c \"Delete :PreferenceSpecifiers:$i\" \"$ROOT_PLIST\"\n ((COUNT--))\n ((i--))\n fi\n fi\ndone\n";
+ shellScript = "#!/bin/sh\n\nPLISTBUDDY=/usr/libexec/PlistBuddy\nROOT_PLIST=\"$SCRIPT_OUTPUT_FILE_0\"\n\nCOUNT=$($PLISTBUDDY -c \"Print :PreferenceSpecifiers\" \"$ROOT_PLIST\" | grep -c '^ }$')\n\nfor ((i = 0; i < COUNT; i++)); do\n remove=0\n platform=$($PLISTBUDDY -c \"Print :PreferenceSpecifiers:$i:Platform\" \"$ROOT_PLIST\" 2> /dev/null)\n if [ ! -z \"$platform\" ]; then\n if [ \"$platform\" == \"$PLATFORM_FAMILY_NAME\" ]; then\n echo \"Found entry $i for $platform\"\n $PLISTBUDDY -c \"Delete :PreferenceSpecifiers:$i:Platform\" \"$ROOT_PLIST\"\n else\n echo \"Exclude $i due to Platform\"\n remove=1\n fi\n fi\n excludetargets=$($PLISTBUDDY -c \"Print :PreferenceSpecifiers:$i:ExcludeTargets\" \"$ROOT_PLIST\" 2> /dev/null | sed '1d;$d' | xargs)\n if [ ! -z \"$excludetargets\" ]; then\n found=0\n for target in $excludetargets; do\n if [ \"$target\" == \"$TARGET_NAME\" ]; then\n found=1\n fi\n done\n if [ $found -eq 1 ]; then\n echo \"Exclude $i due to ExcludeTargets\"\n remove=1\n else\n echo \"Found entry $i for ExcludeTargets\"\n $PLISTBUDDY -c \"Delete :PreferenceSpecifiers:$i:ExcludeTargets\" \"$ROOT_PLIST\"\n fi\n fi\n if [ $remove -eq 1 ]; then\n echo \"Removing entry $i\"\n $PLISTBUDDY -c \"Delete :PreferenceSpecifiers:$i\" \"$ROOT_PLIST\"\n ((COUNT--))\n ((i--))\n fi\ndone\n";
showEnvVarsInLog = 0;
};
CE59A7B22ABCCB7C00E5FFBD /* Patch Settings bundle */ = {
@@ -2891,7 +3406,28 @@
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
- shellScript = "#!/bin/sh\n\nPLISTBUDDY=/usr/libexec/PlistBuddy\nROOT_PLIST=\"$SCRIPT_OUTPUT_FILE_0\"\n\nCOUNT=$($PLISTBUDDY -c \"Print :PreferenceSpecifiers\" \"$ROOT_PLIST\" | grep -c '^ }$')\n\nfor ((i = 0; i < COUNT; i++)); do\n platform=$($PLISTBUDDY -c \"Print :PreferenceSpecifiers:$i:Platform\" \"$ROOT_PLIST\" 2> /dev/null)\n if [ ! -z \"$platform\" ]; then\n if [ \"$platform\" == \"$PLATFORM_FAMILY_NAME\" ]; then\n echo \"Found entry $i for $platform\"\n $PLISTBUDDY -c \"Delete :PreferenceSpecifiers:$i:Platform\" \"$ROOT_PLIST\"\n else\n echo \"Found entry $i for $platform, removing entry\"\n $PLISTBUDDY -c \"Delete :PreferenceSpecifiers:$i\" \"$ROOT_PLIST\"\n ((COUNT--))\n ((i--))\n fi\n fi\ndone\n";
+ shellScript = "#!/bin/sh\n\nPLISTBUDDY=/usr/libexec/PlistBuddy\nROOT_PLIST=\"$SCRIPT_OUTPUT_FILE_0\"\n\nCOUNT=$($PLISTBUDDY -c \"Print :PreferenceSpecifiers\" \"$ROOT_PLIST\" | grep -c '^ }$')\n\nfor ((i = 0; i < COUNT; i++)); do\n remove=0\n platform=$($PLISTBUDDY -c \"Print :PreferenceSpecifiers:$i:Platform\" \"$ROOT_PLIST\" 2> /dev/null)\n if [ ! -z \"$platform\" ]; then\n if [ \"$platform\" == \"$PLATFORM_FAMILY_NAME\" ]; then\n echo \"Found entry $i for $platform\"\n $PLISTBUDDY -c \"Delete :PreferenceSpecifiers:$i:Platform\" \"$ROOT_PLIST\"\n else\n echo \"Exclude $i due to Platform\"\n remove=1\n fi\n fi\n excludetargets=$($PLISTBUDDY -c \"Print :PreferenceSpecifiers:$i:ExcludeTargets\" \"$ROOT_PLIST\" 2> /dev/null | sed '1d;$d' | xargs)\n if [ ! -z \"$excludetargets\" ]; then\n found=0\n for target in $excludetargets; do\n if [ \"$target\" == \"$TARGET_NAME\" ]; then\n found=1\n fi\n done\n if [ $found -eq 1 ]; then\n echo \"Exclude $i due to ExcludeTargets\"\n remove=1\n else\n echo \"Found entry $i for ExcludeTargets\"\n $PLISTBUDDY -c \"Delete :PreferenceSpecifiers:$i:ExcludeTargets\" \"$ROOT_PLIST\"\n fi\n fi\n if [ $remove -eq 1 ]; then\n echo \"Removing entry $i\"\n $PLISTBUDDY -c \"Delete :PreferenceSpecifiers:$i\" \"$ROOT_PLIST\"\n ((COUNT--))\n ((i--))\n fi\ndone\n";
+ showEnvVarsInLog = 0;
+ };
+ CEF7F6812AEEDCC400E34952 /* Patch Settings bundle */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ );
+ inputPaths = (
+ "$(SRCROOT)/Platform/iOS/Settings.bundle",
+ );
+ name = "Patch Settings bundle";
+ outputFileListPaths = (
+ );
+ outputPaths = (
+ "$(TARGET_BUILD_DIR)/$(CONTENTS_FOLDER_PATH)/Settings.bundle/Root.plist",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "#!/bin/sh\n\nPLISTBUDDY=/usr/libexec/PlistBuddy\nROOT_PLIST=\"$SCRIPT_OUTPUT_FILE_0\"\n\nCOUNT=$($PLISTBUDDY -c \"Print :PreferenceSpecifiers\" \"$ROOT_PLIST\" | grep -c '^ }$')\n\nfor ((i = 0; i < COUNT; i++)); do\n remove=0\n platform=$($PLISTBUDDY -c \"Print :PreferenceSpecifiers:$i:Platform\" \"$ROOT_PLIST\" 2> /dev/null)\n if [ ! -z \"$platform\" ]; then\n if [ \"$platform\" == \"$PLATFORM_FAMILY_NAME\" ]; then\n echo \"Found entry $i for $platform\"\n $PLISTBUDDY -c \"Delete :PreferenceSpecifiers:$i:Platform\" \"$ROOT_PLIST\"\n else\n echo \"Exclude $i due to Platform\"\n remove=1\n fi\n fi\n excludetargets=$($PLISTBUDDY -c \"Print :PreferenceSpecifiers:$i:ExcludeTargets\" \"$ROOT_PLIST\" 2> /dev/null | sed '1d;$d' | xargs)\n if [ ! -z \"$excludetargets\" ]; then\n found=0\n for target in $excludetargets; do\n if [ \"$target\" == \"$TARGET_NAME\" ]; then\n found=1\n fi\n done\n if [ $found -eq 1 ]; then\n echo \"Exclude $i due to ExcludeTargets\"\n remove=1\n else\n echo \"Found entry $i for ExcludeTargets\"\n $PLISTBUDDY -c \"Delete :PreferenceSpecifiers:$i:ExcludeTargets\" \"$ROOT_PLIST\"\n fi\n fi\n if [ $remove -eq 1 ]; then\n echo \"Removing entry $i\"\n $PLISTBUDDY -c \"Delete :PreferenceSpecifiers:$i\" \"$ROOT_PLIST\"\n ((COUNT--))\n ((i--))\n fi\ndone\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */
@@ -2956,6 +3492,7 @@
CEF0305126A2AFBF00667B63 /* Spinner.swift in Sources */,
843BF840284555E70029D60D /* UTMQemuConfigurationPortForward.swift in Sources */,
CE611BE729F50CAD001817BC /* UTMReleaseHelper.swift in Sources */,
+ CEF01DB22B6724A300725A0F /* UTMSpiceVirtualMachine.swift in Sources */,
CE2D958324AD4F990059923A /* VMConfigNetworkView.swift in Sources */,
CE2D929C24AD46670059923A /* UTMLegacyViewState.m in Sources */,
CE020BAB24AEE00000B44AB6 /* UTMLoggingSwift.swift in Sources */,
@@ -2983,6 +3520,7 @@
CED814E924C79F070042F0F1 /* VMConfigDriveCreateView.swift in Sources */,
842B9F8D28CC58B700031EE7 /* UTMPatches.swift in Sources */,
CE19392626DCB094005CEC17 /* RAMSlider.swift in Sources */,
+ CE9B15412B11A74E003A32DD /* UTMRemoteKeyManager.swift in Sources */,
CE611BEB29F50D3E001817BC /* VMReleaseNotesView.swift in Sources */,
CEE7E936287CFDB100282049 /* UTMLegacyQemuConfiguration+Constants.m in Sources */,
4B224B9D279D4D8100B63CFF /* InListButtonStyle.swift in Sources */,
@@ -2997,6 +3535,7 @@
CE2D957524AD4F990059923A /* VMConfigInputView.swift in Sources */,
CEF0305B26A2AFDF00667B63 /* VMWizardOSOtherView.swift in Sources */,
84C60FB72681A41B00B58C00 /* VMToolbarView.swift in Sources */,
+ CEF01DB72B674BF000725A0F /* UTMPipeInterface.swift in Sources */,
CE2D92CB24AD46670059923A /* VMDisplayMetalViewController+Gamepad.m in Sources */,
CEF0307426A2B40B00667B63 /* VMWizardHardwareView.swift in Sources */,
841E997528AA1191003C6CB6 /* UTMRegistry.swift in Sources */,
@@ -3056,6 +3595,7 @@
84C505AC28C588EC007CE8FF /* SizeTextField.swift in Sources */,
8471770627CC974F00D3A50B /* DefaultTextField.swift in Sources */,
84E6F6FD289319AE00080EEF /* VMToolbarDisplayMenuView.swift in Sources */,
+ CE9B15472B12A87E003A32DD /* GenerateKey.c in Sources */,
CE8813D324CD230300532628 /* ActivityView.swift in Sources */,
CEDF83F9258AE24E0030E4AC /* UTMPasteboard.swift in Sources */,
848D99B4286300160055C215 /* QEMUArgument.swift in Sources */,
@@ -3076,6 +3616,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
+ CEE06B272B2FC89400A811AE /* UTMServerView.swift in Sources */,
CEB63A7724F4654400CAF323 /* Main.swift in Sources */,
84E3A91B2946D2590024A740 /* UTMMenuBarExtraScene.swift in Sources */,
CEB63A7B24F469E300CAF323 /* UTMJailbreak.m in Sources */,
@@ -3084,6 +3625,7 @@
2C6D9E03256EE454003298E6 /* VMDisplayQemuTerminalWindowController.swift in Sources */,
CE6D21DD2553A6ED001D29C5 /* VMConfirmActionModifier.swift in Sources */,
85EC516627CC8D10004A51DE /* VMConfigAdvancedNetworkView.swift in Sources */,
+ CE1AEC402B78B30700992AFC /* MacDeviceLabel.swift in Sources */,
CE020BB724B14F8400B44AB6 /* UTMVirtualMachine.swift in Sources */,
845F170B289CB07200944904 /* VMDisplayAppleDisplayWindowController.swift in Sources */,
CE772AAD25C8B0F600E4E379 /* ContentView.swift in Sources */,
@@ -3112,6 +3654,7 @@
8432329628C2ED9000CFBC97 /* FileBrowseField.swift in Sources */,
848A98C2286A2257006F0550 /* UTMAppleConfigurationMacPlatform.swift in Sources */,
84B36D2B27B790BE00C22685 /* DestructiveButton.swift in Sources */,
+ CE9B154A2B12A87E003A32DD /* GenerateKey.c in Sources */,
CE020BAC24AEE00000B44AB6 /* UTMLoggingSwift.swift in Sources */,
848D99BA28630A780055C215 /* VMConfigSerialView.swift in Sources */,
8401FDA2269D3E2500265F0D /* VMConfigAppleNetworkingView.swift in Sources */,
@@ -3179,6 +3722,7 @@
CE25125129C806AF000790AB /* UTMScriptingDeleteCommand.swift in Sources */,
CE0B6CFB24AD568400FE012D /* UTMLegacyQemuConfiguration+Networking.m in Sources */,
84C584E5268F8C65000FCABF /* VMAppleSettingsView.swift in Sources */,
+ CE9B15442B11A74E003A32DD /* UTMRemoteKeyManager.swift in Sources */,
84F746BB276FF70700A20C87 /* VMDisplayQemuDisplayController.swift in Sources */,
CE772AB425C8B7B500E4E379 /* VMCommands.swift in Sources */,
CE2D958424AD4F990059923A /* VMConfigNetworkView.swift in Sources */,
@@ -3198,6 +3742,7 @@
CEC0A30A2A7490D200980857 /* VMConfigQEMUArgumentsView.swift in Sources */,
843232B928C4816100CFBC97 /* UTMDownloadSupportToolsTask.swift in Sources */,
8471772A27CD3CAB00D3A50B /* DetailedSection.swift in Sources */,
+ CEF01DB52B6724A300725A0F /* UTMSpiceVirtualMachine.swift in Sources */,
8432329A28C3084A00CFBC97 /* GlobalFileImporter.swift in Sources */,
CE19392826DCB094005CEC17 /* RAMSlider.swift in Sources */,
2C33B3AA2566C9B100A954A6 /* VMContextMenuModifier.swift in Sources */,
@@ -3208,6 +3753,7 @@
CEF0305D26A2AFDF00667B63 /* VMWizardOSOtherView.swift in Sources */,
CEEC811B24E48EC700ACB0B3 /* SettingsView.swift in Sources */,
8443EFF42845641600B2E6E2 /* UTMQemuConfigurationDrive.swift in Sources */,
+ CEFE96772B69A7CC000F00C9 /* VMRemoteSessionState.swift in Sources */,
CE2D957024AD4F990059923A /* VMRemovableDrivesView.swift in Sources */,
CE25124B29BFE273000790AB /* UTMScriptable.swift in Sources */,
CE0B6CFE24AD56AE00FE012D /* UTMLogging.m in Sources */,
@@ -3222,6 +3768,7 @@
848F71EE277A2F47006A0240 /* UTMSerialPortDelegate.swift in Sources */,
848A98BA286A17A8006F0550 /* UTMAppleConfigurationNetwork.swift in Sources */,
84018699288B71BF0050AC51 /* BusyIndicator.swift in Sources */,
+ CEF01DB92B674BF000725A0F /* UTMPipeInterface.swift in Sources */,
CEB54C852931E32F000D2AA9 /* UTMPatches.swift in Sources */,
84C2E8672AA429E800B17308 /* VMWizardContent.swift in Sources */,
848A98C0286A20E3006F0550 /* UTMAppleConfigurationBoot.swift in Sources */,
@@ -3229,6 +3776,7 @@
CE2D958A24AD4F990059923A /* VMConfigSystemView.swift in Sources */,
CEBBF1A724B5730F00C15049 /* UTMDataExtension.swift in Sources */,
84C584E3268F8AE7000FCABF /* VMQEMUSettingsView.swift in Sources */,
+ CE6C13CB2B63610C003B7032 /* UTMRemoteMessage.swift in Sources */,
845F1707289B5E2600944904 /* VMAppleSettingsAddDeviceMenuView.swift in Sources */,
843BF83E2845494C0029D60D /* UTMQemuConfigurationSerial.swift in Sources */,
841E997728AA1191003C6CB6 /* UTMRegistry.swift in Sources */,
@@ -3245,6 +3793,7 @@
CE020BA424AEDC7C00B44AB6 /* UTMData.swift in Sources */,
CE25124929BFDBA6000790AB /* UTMScriptingGuestFileImpl.swift in Sources */,
848A98C8287206AE006F0550 /* VMConfigAppleVirtualizationView.swift in Sources */,
+ CE9B153F2B11A63E003A32DD /* UTMRemoteServer.swift in Sources */,
847BF9AC2A49C783000BD9AA /* VMData.swift in Sources */,
CE25124729BFDB87000790AB /* UTMScriptingGuestProcessImpl.swift in Sources */,
CE2D958824AD4F990059923A /* VMConfigPortForwardForm.swift in Sources */,
@@ -3282,6 +3831,7 @@
841619AF28431952000034B2 /* UTMQemuConfigurationSystem.swift in Sources */,
8432329528C2ED9000CFBC97 /* FileBrowseField.swift in Sources */,
843BF82528441EAD0029D60D /* UTMQemuConfigurationDisplay.swift in Sources */,
+ CE9B15422B11A74E003A32DD /* UTMRemoteKeyManager.swift in Sources */,
CEA45E3F263519B5002FA97D /* UTMProcess.m in Sources */,
CEA45E43263519B5002FA97D /* UTMLegacyQemuConfigurationPortForward.m in Sources */,
843BF841284555E70029D60D /* UTMQemuConfigurationPortForward.swift in Sources */,
@@ -3300,6 +3850,7 @@
CEA45E5A263519B5002FA97D /* VMConfigSystemView.swift in Sources */,
CEA45E5B263519B5002FA97D /* VMShareFileModifier.swift in Sources */,
8471770727CC974F00D3A50B /* DefaultTextField.swift in Sources */,
+ CE9B15482B12A87E003A32DD /* GenerateKey.c in Sources */,
8432329128C2CDAD00CFBC97 /* VMNavigationListView.swift in Sources */,
CEA45E61263519B5002FA97D /* VMConfigNetworkView.swift in Sources */,
84018698288B71BF0050AC51 /* BusyIndicator.swift in Sources */,
@@ -3354,6 +3905,7 @@
CEF0305226A2AFBF00667B63 /* Spinner.swift in Sources */,
CEA45EBD263519B5002FA97D /* UTMQemuSystem.m in Sources */,
CEA45EBE263519B5002FA97D /* NumberTextField.swift in Sources */,
+ CEF01DB32B6724A300725A0F /* UTMSpiceVirtualMachine.swift in Sources */,
843232B828C4816100CFBC97 /* UTMDownloadSupportToolsTask.swift in Sources */,
CEF0306526A2AFDF00667B63 /* VMWizardOSLinuxView.swift in Sources */,
CEA45EC3263519B5002FA97D /* VMCommands.swift in Sources */,
@@ -3372,12 +3924,12 @@
CE19392726DCB094005CEC17 /* RAMSlider.swift in Sources */,
84E6F6FE289319AE00080EEF /* VMToolbarDisplayMenuView.swift in Sources */,
841E58CC28937EE200137A20 /* UTMExternalSceneDelegate.swift in Sources */,
+ CEF01DB82B674BF000725A0F /* UTMPipeInterface.swift in Sources */,
CEF0307226A2B04400667B63 /* VMWizardView.swift in Sources */,
83034C0826AB630F006B4BAF /* UTMPendingVMView.swift in Sources */,
CEA45ED8263519B5002FA97D /* VMKeyboardButton.m in Sources */,
CEF0307526A2B40B00667B63 /* VMWizardHardwareView.swift in Sources */,
CE8011212AD4E9E8009001C2 /* UTMApp.swift in Sources */,
- CEA45EDF263519B5002FA97D /* UTMJailbreak.m in Sources */,
CE611BEC29F50D3E001817BC /* VMReleaseNotesView.swift in Sources */,
CE611BE829F50CAD001817BC /* UTMReleaseHelper.swift in Sources */,
CEA45EE8263519B5002FA97D /* VMDisplayMetalViewController+Keyboard.m in Sources */,
@@ -3427,6 +3979,159 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
+ CEF7F5962AEEDCC400E34952 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ CEF7F5972AEEDCC400E34952 /* VMDisplayMetalViewController+Pointer.h in Sources */,
+ CEF7F5982AEEDCC400E34952 /* VMSettingsAddDeviceMenuView.swift in Sources */,
+ CEF7F5992AEEDCC400E34952 /* VMRemovableDrivesView.swift in Sources */,
+ CEF7F59A2AEEDCC400E34952 /* UTMQemuConfigurationDrive.swift in Sources */,
+ CEE06B292B30013500A811AE /* UTMRemoteConnectView.swift in Sources */,
+ CEF7F59B2AEEDCC400E34952 /* UTMQemuConfigurationSharing.swift in Sources */,
+ CEF7F59C2AEEDCC400E34952 /* ContentView.swift in Sources */,
+ CEF7F59D2AEEDCC400E34952 /* VMData.swift in Sources */,
+ CEF7F59E2AEEDCC400E34952 /* UTMLegacyQemuConfiguration+System.m in Sources */,
+ CEF7F59F2AEEDCC400E34952 /* UTMQemuConfigurationNetwork.swift in Sources */,
+ CE70E8D52B648FBE007FA787 /* UTMRemoteSpiceVirtualMachine.swift in Sources */,
+ CEF7F5A12AEEDCC400E34952 /* VMWizardDrivesView.swift in Sources */,
+ CEF7F5A22AEEDCC400E34952 /* VMWindowState.swift in Sources */,
+ CEF7F5A42AEEDCC400E34952 /* UTMLegacyQemuConfigurationPortForward.m in Sources */,
+ CEF7F5A52AEEDCC400E34952 /* VMWizardView.swift in Sources */,
+ CEF7F5A62AEEDCC400E34952 /* UTMPlaceholderVMView.swift in Sources */,
+ CEF7F5A72AEEDCC400E34952 /* BusyOverlay.swift in Sources */,
+ CE9B15432B11A74E003A32DD /* UTMRemoteKeyManager.swift in Sources */,
+ CEF7F5A82AEEDCC400E34952 /* UTMConfiguration.swift in Sources */,
+ CEF7F5A92AEEDCC400E34952 /* UTMConfigurationInfo.swift in Sources */,
+ CEF7F5AA2AEEDCC400E34952 /* VMConfigDisplayView.swift in Sources */,
+ CEF7F5AB2AEEDCC400E34952 /* VMWizardOSWindowsView.swift in Sources */,
+ CEF7F5AC2AEEDCC400E34952 /* UTMDownloadTask.swift in Sources */,
+ CEF7F5AD2AEEDCC400E34952 /* UTMApp.swift in Sources */,
+ CEF7F5AE2AEEDCC400E34952 /* VMConfigAdvancedNetworkView.swift in Sources */,
+ CEF7F5AF2AEEDCC400E34952 /* UTMLegacyQemuConfiguration+Miscellaneous.m in Sources */,
+ CEF7F5B02AEEDCC400E34952 /* UTMRegistryEntry.swift in Sources */,
+ CEF7F5B12AEEDCC400E34952 /* UTMDataExtension.swift in Sources */,
+ CEF7F5B22AEEDCC400E34952 /* VMDisplayHostedView.swift in Sources */,
+ CEF7F5B32AEEDCC400E34952 /* QEMUArgumentBuilder.swift in Sources */,
+ CEF7F5B42AEEDCC400E34952 /* ImagePicker.swift in Sources */,
+ CEF7F5B52AEEDCC400E34952 /* VMConfigSystemView.swift in Sources */,
+ CEF7F5B62AEEDCC400E34952 /* FileBrowseField.swift in Sources */,
+ CEF7F5B72AEEDCC400E34952 /* VMShareFileModifier.swift in Sources */,
+ CEF7F5B82AEEDCC400E34952 /* UTMQemuConfigurationSerial.swift in Sources */,
+ CEF7F5B92AEEDCC400E34952 /* Spinner.swift in Sources */,
+ CE9B15492B12A87E003A32DD /* GenerateKey.c in Sources */,
+ CEF7F5BA2AEEDCC400E34952 /* UTMQemuConfigurationPortForward.swift in Sources */,
+ CEF7F5BB2AEEDCC400E34952 /* UTMReleaseHelper.swift in Sources */,
+ CEF7F5BC2AEEDCC400E34952 /* VMConfigNetworkView.swift in Sources */,
+ CEF7F5BD2AEEDCC400E34952 /* UTMLegacyViewState.m in Sources */,
+ CEF7F5BF2AEEDCC400E34952 /* VMWizardOSLinuxView.swift in Sources */,
+ CEF7F5C02AEEDCC400E34952 /* VMWizardSummaryView.swift in Sources */,
+ CEF7F5C12AEEDCC400E34952 /* VMConfigQEMUView.swift in Sources */,
+ CEF7F5C22AEEDCC400E34952 /* VMDisplayMetalViewController+Touch.m in Sources */,
+ CEF7F5C32AEEDCC400E34952 /* UTMLegacyQemuConfiguration+Display.m in Sources */,
+ CEF7F5C42AEEDCC400E34952 /* BigButtonStyle.swift in Sources */,
+ CEF7F5C52AEEDCC400E34952 /* UTMVirtualMachine.swift in Sources */,
+ CEF7F5C62AEEDCC400E34952 /* UTMQemuConfigurationSystem.swift in Sources */,
+ CEF7F5C72AEEDCC400E34952 /* VMWizardSharingView.swift in Sources */,
+ CEF7F5C82AEEDCC400E34952 /* VMConfigInfoView.swift in Sources */,
+ CEF7F5C92AEEDCC400E34952 /* MenuLabel.swift in Sources */,
+ CEF7F5CA2AEEDCC400E34952 /* VMKeyboardView.m in Sources */,
+ CEF7F5CB2AEEDCC400E34952 /* VMWizardOSView.swift in Sources */,
+ CEF7F5CC2AEEDCC400E34952 /* DestructiveButton.swift in Sources */,
+ CEF7F5CD2AEEDCC400E34952 /* UTMConfigurationTerminal.swift in Sources */,
+ CEF7F5CE2AEEDCC400E34952 /* VMWindowView.swift in Sources */,
+ CEF7F5CF2AEEDCC400E34952 /* UTMPendingVMView.swift in Sources */,
+ CEE8B4C32B71E2BA0035AE86 /* UTMLoggingSwift.swift in Sources */,
+ CEF7F5D02AEEDCC400E34952 /* UTMSpiceIO.m in Sources */,
+ CEF7F5D12AEEDCC400E34952 /* UTMUnavailableVMView.swift in Sources */,
+ CEF7F5D22AEEDCC400E34952 /* VMDrivesSettingsView.swift in Sources */,
+ CEF7F5D32AEEDCC400E34952 /* UTMConfigurationDrive.swift in Sources */,
+ CEF7F5D42AEEDCC400E34952 /* VMConfigDriveCreateView.swift in Sources */,
+ CE1AEC3F2B78B30700992AFC /* MacDeviceLabel.swift in Sources */,
+ CEF7F5D52AEEDCC400E34952 /* UTMPatches.swift in Sources */,
+ CEF7F5D62AEEDCC400E34952 /* RAMSlider.swift in Sources */,
+ CEF7F5D72AEEDCC400E34952 /* VMReleaseNotesView.swift in Sources */,
+ CEF7F5D82AEEDCC400E34952 /* UTMLegacyQemuConfiguration+Constants.m in Sources */,
+ CEF7F5D92AEEDCC400E34952 /* InListButtonStyle.swift in Sources */,
+ CEF7F5DA2AEEDCC400E34952 /* VMContextMenuModifier.swift in Sources */,
+ CEF7F5DB2AEEDCC400E34952 /* VMDisplayMetalViewController+Pencil.m in Sources */,
+ CEF7F5DC2AEEDCC400E34952 /* VMDisplayTerminalViewController.swift in Sources */,
+ CEF7F5DD2AEEDCC400E34952 /* UTMLegacyQemuConfiguration+Drives.m in Sources */,
+ CEF7F5DE2AEEDCC400E34952 /* UTMPendingVirtualMachine.swift in Sources */,
+ CEF7F5DF2AEEDCC400E34952 /* BusyIndicator.swift in Sources */,
+ CEF7F5E02AEEDCC400E34952 /* VMSessionState.swift in Sources */,
+ CEF7F5E12AEEDCC400E34952 /* VMConfigSharingView.swift in Sources */,
+ CEF7F5E22AEEDCC400E34952 /* VMConfigInputView.swift in Sources */,
+ CEF7F5E32AEEDCC400E34952 /* VMWizardOSOtherView.swift in Sources */,
+ CEF7F5E42AEEDCC400E34952 /* VMToolbarView.swift in Sources */,
+ CEF7F5E52AEEDCC400E34952 /* VMDisplayMetalViewController+Gamepad.m in Sources */,
+ CEF7F5E62AEEDCC400E34952 /* VMWizardHardwareView.swift in Sources */,
+ CEF7F5E72AEEDCC400E34952 /* UTMRegistry.swift in Sources */,
+ CEF7F5E82AEEDCC400E34952 /* VMDisplayViewControllerDelegate.swift in Sources */,
+ CEF7F5EA2AEEDCC400E34952 /* VMConfigConstantPicker.swift in Sources */,
+ CEF7F5EC2AEEDCC400E34952 /* VMToolbarModifier.swift in Sources */,
+ CEF7F5ED2AEEDCC400E34952 /* VMCursor.m in Sources */,
+ CEF7F5EE2AEEDCC400E34952 /* VMConfigDriveDetailsView.swift in Sources */,
+ CEF7F5F02AEEDCC400E34952 /* NumberTextField.swift in Sources */,
+ CEF7F5F12AEEDCC400E34952 /* VMToolbarOrnamentModifier.swift in Sources */,
+ CEF01DB42B6724A300725A0F /* UTMSpiceVirtualMachine.swift in Sources */,
+ CEF7F5F22AEEDCC400E34952 /* VMCommands.swift in Sources */,
+ CEF7F5F32AEEDCC400E34952 /* UTMLegacyQemuConfiguration+Networking.m in Sources */,
+ CEF7F5F42AEEDCC400E34952 /* VMConfirmActionModifier.swift in Sources */,
+ CEF7F5F52AEEDCC400E34952 /* QEMUConstant.swift in Sources */,
+ CEF7F5F62AEEDCC400E34952 /* VMConfigPortForwardForm.swift in Sources */,
+ CEF7F5F82AEEDCC400E34952 /* DetailedSection.swift in Sources */,
+ CEF7F5F92AEEDCC400E34952 /* VMToolbarDriveMenuView.swift in Sources */,
+ CE08334B2B784FD400522C03 /* RemoteContentView.swift in Sources */,
+ CEF7F5FA2AEEDCC400E34952 /* VMSettingsView.swift in Sources */,
+ CEF7F5FB2AEEDCC400E34952 /* VMDisplayViewController.swift in Sources */,
+ CEF7F5FC2AEEDCC400E34952 /* VMWizardStartView.swift in Sources */,
+ CEF7F5FD2AEEDCC400E34952 /* QEMUConstantGenerated.swift in Sources */,
+ CEF7F5FE2AEEDCC400E34952 /* VMKeyboardButton.m in Sources */,
+ CEF7F5FF2AEEDCC400E34952 /* UTMDownloadVMTask.swift in Sources */,
+ CEF7F6002AEEDCC400E34952 /* GlobalFileImporter.swift in Sources */,
+ CEF7F6022AEEDCC400E34952 /* VMWizardContent.swift in Sources */,
+ CEF7F6032AEEDCC400E34952 /* UTMExternalSceneDelegate.swift in Sources */,
+ CEF7F6052AEEDCC400E34952 /* UTMQemuConfigurationQEMU.swift in Sources */,
+ CEF7F6062AEEDCC400E34952 /* UTMQemuConfigurationDisplay.swift in Sources */,
+ CEE8B4C22B71E0FB0035AE86 /* UTMLogging.m in Sources */,
+ CEF7F6072AEEDCC400E34952 /* UTMApp.swift in Sources */,
+ CEF7F6082AEEDCC400E34952 /* VMConfigDisplayConsoleView.swift in Sources */,
+ CEF7F60A2AEEDCC400E34952 /* VMConfigSerialView.swift in Sources */,
+ CE38EC692B5DB3AE008B324B /* UTMRemoteClient.swift in Sources */,
+ CEF7F60B2AEEDCC400E34952 /* VMWizardState.swift in Sources */,
+ CEF7F60C2AEEDCC400E34952 /* UTMQemuConfigurationInput.swift in Sources */,
+ CEF7F60D2AEEDCC400E34952 /* VMDisplayMetalViewController+Keyboard.m in Sources */,
+ CEF7F60E2AEEDCC400E34952 /* UTMExtensions.swift in Sources */,
+ CEF7F60F2AEEDCC400E34952 /* UTMData.swift in Sources */,
+ CEF7F6112AEEDCC400E34952 /* VMConfigSoundView.swift in Sources */,
+ CEF7F6122AEEDCC400E34952 /* UTMLegacyQemuConfiguration.m in Sources */,
+ CEF7F6142AEEDCC400E34952 /* VMDetailsView.swift in Sources */,
+ CEF7F6152AEEDCC400E34952 /* VMDisplayMetalViewController.m in Sources */,
+ CEF7F6162AEEDCC400E34952 /* UTMQemuConfiguration+Arguments.swift in Sources */,
+ CEF7F6172AEEDCC400E34952 /* Main.swift in Sources */,
+ CEF7F6192AEEDCC400E34952 /* VMCardView.swift in Sources */,
+ CEF7F61A2AEEDCC400E34952 /* VMNavigationListView.swift in Sources */,
+ CEF7F61B2AEEDCC400E34952 /* UTMSingleWindowView.swift in Sources */,
+ CEF7F61C2AEEDCC400E34952 /* UTMLegacyQemuConfiguration+Sharing.m in Sources */,
+ CEF7F61D2AEEDCC400E34952 /* SizeTextField.swift in Sources */,
+ CEF7F61E2AEEDCC400E34952 /* DefaultTextField.swift in Sources */,
+ CEF7F61F2AEEDCC400E34952 /* VMToolbarDisplayMenuView.swift in Sources */,
+ CEF7F6202AEEDCC400E34952 /* ActivityView.swift in Sources */,
+ CEF7F6212AEEDCC400E34952 /* UTMPasteboard.swift in Sources */,
+ CE6C13CA2B63610C003B7032 /* UTMRemoteMessage.swift in Sources */,
+ CEF7F6222AEEDCC400E34952 /* QEMUArgument.swift in Sources */,
+ CEF7F6232AEEDCC400E34952 /* VMPlaceholderView.swift in Sources */,
+ CEF7F6242AEEDCC400E34952 /* VMDisplayMetalViewController+Pointer.m in Sources */,
+ CEF7F6252AEEDCC400E34952 /* VMDisplayViewController.m in Sources */,
+ CEF7F6282AEEDCC400E34952 /* UTMQemuConfigurationSound.swift in Sources */,
+ CEF7F6292AEEDCC400E34952 /* VMScroll.m in Sources */,
+ CEF7F62A2AEEDCC400E34952 /* VMConfigNetworkPortForwardView.swift in Sources */,
+ CEF7F62B2AEEDCC400E34952 /* UTMDownloadSupportToolsTask.swift in Sources */,
+ CEF7F62C2AEEDCC400E34952 /* UTMQemuConfiguration.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
@@ -3477,6 +4182,7 @@
CEB54C16293009C7000D2AA9 /* pl */,
F6DA2DA92AAFED5F0070DCD1 /* zh-HK */,
61EBDEA22AACA83100B959A2 /* ru */,
+ 037DAA1F2B0B92580061ACB3 /* it */,
);
name = Localizable.strings;
sourceTree = "";
@@ -3505,6 +4211,7 @@
F6DA2DA62AAFED5F0070DCD1 /* zh-HK */,
F6DA2DAF2AAFEE060070DCD1 /* zh-Hans */,
61EBDE9F2AACA83100B959A2 /* ru */,
+ 037DAA1C2B0B92580061ACB3 /* it */,
);
name = VMDisplayWindow.xib;
sourceTree = "";
@@ -3526,6 +4233,15 @@
name = VMDisplayMetalViewInputAccessory.xib;
sourceTree = "";
};
+ CEB5C1192B8C4CD4008AAE5C /* Info-RemotePlist.strings */ = {
+ isa = PBXVariantGroup;
+ children = (
+ CEB5C1182B8C4CD4008AAE5C /* en */,
+ CEB5C11A2B8C4D30008AAE5C /* ja */,
+ );
+ name = "Info-RemotePlist.strings";
+ sourceTree = "";
+ };
CED8DF7928A120C100C34345 /* Localizable.stringsdict */ = {
isa = PBXVariantGroup;
children = (
@@ -3539,6 +4255,7 @@
F6DA2DAA2AAFED5F0070DCD1 /* zh-HK */,
F6DA2DB12AAFF0640070DCD1 /* zh-Hans */,
61EBDEA32AACA83100B959A2 /* ru */,
+ 037DAA202B0B92580061ACB3 /* it */,
);
name = Localizable.stringsdict;
sourceTree = "";
@@ -3566,6 +4283,7 @@
83FE63B828F617CE0047FFEF /* de */,
F6DA2DA72AAFED5F0070DCD1 /* zh-HK */,
61EBDEA02AACA83100B959A2 /* ru */,
+ 037DAA1D2B0B92580061ACB3 /* it */,
);
name = InfoPlist.strings;
sourceTree = "";
@@ -3584,6 +4302,7 @@
CEB54C14293009C6000D2AA9 /* pl */,
F6DA2DA82AAFED5F0070DCD1 /* zh-HK */,
61EBDEA12AACA83100B959A2 /* ru */,
+ 037DAA1E2B0B92580061ACB3 /* it */,
);
name = InfoPlist.strings;
sourceTree = "";
@@ -3602,6 +4321,7 @@
9786BB59294056960032B858 /* ja */,
F6DA2DAC2AAFED5F0070DCD1 /* zh-HK */,
61EBDEA52AACA83100B959A2 /* ru */,
+ 037DAA222B0B92580061ACB3 /* it */,
);
name = InfoPlist.strings;
sourceTree = "";
@@ -3620,6 +4340,7 @@
CEB54C1829300C1B000D2AA9 /* pl */,
F6DA2DAB2AAFED5F0070DCD1 /* zh-HK */,
61EBDEA42AACA83100B959A2 /* ru */,
+ 037DAA212B0B92580061ACB3 /* it */,
);
name = Localizable.strings;
sourceTree = "";
@@ -3720,6 +4441,12 @@
CODE_SIGN_IDENTITY = "$(CODE_SIGN_IDENTITY_IOS:default=Apple Development)";
ENABLE_BITCODE = NO;
ENABLE_PREVIEWS = YES;
+ GCC_PREPROCESSOR_DEFINITIONS = (
+ "WITH_JIT=1",
+ "WITH_SOLO_VM=1",
+ "WITH_USB=1",
+ "$(inherited)",
+ );
INFOPLIST_FILE = Platform/iOS/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
@@ -3731,6 +4458,7 @@
SUPPORTS_MACCATALYST = NO;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
+ SWIFT_ACTIVE_COMPILATION_CONDITIONS = "WITH_JIT WITH_SOLO_VM WITH_USB $(inherited)";
SWIFT_OBJC_BRIDGING_HEADER = "Services/Swift-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
@@ -3748,6 +4476,12 @@
CODE_SIGN_IDENTITY = "$(CODE_SIGN_IDENTITY_IOS:default=Apple Development)";
ENABLE_BITCODE = NO;
ENABLE_PREVIEWS = YES;
+ GCC_PREPROCESSOR_DEFINITIONS = (
+ "WITH_JIT=1",
+ "WITH_SOLO_VM=1",
+ "WITH_USB=1",
+ "$(inherited)",
+ );
INFOPLIST_FILE = Platform/iOS/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
@@ -3759,6 +4493,7 @@
SUPPORTS_MACCATALYST = NO;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
+ SWIFT_ACTIVE_COMPILATION_CONDITIONS = "WITH_JIT WITH_SOLO_VM WITH_USB $(inherited)";
SWIFT_OBJC_BRIDGING_HEADER = "Services/Swift-Bridging-Header.h";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2,7";
@@ -3777,6 +4512,12 @@
CODE_SIGN_STYLE = Manual;
COMBINE_HIDPI_IMAGES = YES;
ENABLE_PREVIEWS = YES;
+ GCC_PREPROCESSOR_DEFINITIONS = (
+ "WITH_JIT=1",
+ "WITH_SERVER=1",
+ "WITH_USB=1",
+ "$(inherited)",
+ );
INFOPLIST_FILE = Platform/macOS/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
@@ -3786,7 +4527,7 @@
PRODUCT_NAME = "$(PROJECT_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "$(PROVISIONING_PROFILE_SPECIFIER_MAC:default=)";
SUPPORTED_PLATFORMS = macosx;
- SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
+ SWIFT_ACTIVE_COMPILATION_CONDITIONS = "WITH_JIT WITH_SERVER WITH_USB $(inherited)";
SWIFT_OBJC_BRIDGING_HEADER = "Services/Swift-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
@@ -3805,6 +4546,12 @@
CODE_SIGN_STYLE = Manual;
COMBINE_HIDPI_IMAGES = YES;
ENABLE_PREVIEWS = YES;
+ GCC_PREPROCESSOR_DEFINITIONS = (
+ "WITH_JIT=1",
+ "WITH_SERVER=1",
+ "WITH_USB=1",
+ "$(inherited)",
+ );
INFOPLIST_FILE = Platform/macOS/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
@@ -3814,6 +4561,7 @@
PRODUCT_NAME = "$(PROJECT_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "$(PROVISIONING_PROFILE_SPECIFIER_MAC:default=)";
SUPPORTED_PLATFORMS = macosx;
+ SWIFT_ACTIVE_COMPILATION_CONDITIONS = "WITH_JIT WITH_SERVER WITH_USB $(inherited)";
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OBJC_BRIDGING_HEADER = "Services/Swift-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-O";
@@ -3902,6 +4650,7 @@
QUOTED_SYSROOT_DIR = "\"$(SYSROOT_DIR)\"";
SDKROOT = auto;
SUPPORTED_PLATFORMS = "";
+ SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG";
SYSROOT_DIR = "sysroot-$(PLATFORM_DISPLAY_NAME:identifier)$(PLATFORM_SUFFIX)-$(ARCHS:identifier)";
};
name = Debug;
@@ -3980,6 +4729,7 @@
QUOTED_SYSROOT_DIR = "\"$(SYSROOT_DIR)\"";
SDKROOT = auto;
SUPPORTED_PLATFORMS = "";
+ SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited)";
SWIFT_COMPILATION_MODE = wholemodule;
SYSROOT_DIR = "sysroot-$(PLATFORM_DISPLAY_NAME:identifier)$(PLATFORM_SUFFIX)-$(ARCHS:identifier)";
VALIDATE_PRODUCT = YES;
@@ -4050,6 +4800,7 @@
ENABLE_PREVIEWS = YES;
GCC_PREPROCESSOR_DEFINITIONS = (
"WITH_QEMU_TCI=1",
+ "WITH_SOLO_VM=1",
"$(inherited)",
);
INFOPLIST_FILE = Platform/iOS/Info.plist;
@@ -4065,7 +4816,7 @@
SUPPORTS_MACCATALYST = NO;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
- SWIFT_ACTIVE_COMPILATION_CONDITIONS = WITH_QEMU_TCI;
+ SWIFT_ACTIVE_COMPILATION_CONDITIONS = "WITH_QEMU_TCI WITH_SOLO_VM $(inherited)";
SWIFT_OBJC_BRIDGING_HEADER = "Services/Swift-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
@@ -4085,6 +4836,7 @@
ENABLE_PREVIEWS = YES;
GCC_PREPROCESSOR_DEFINITIONS = (
"WITH_QEMU_TCI=1",
+ "WITH_SOLO_VM=1",
"$(inherited)",
);
INFOPLIST_FILE = Platform/iOS/Info.plist;
@@ -4100,7 +4852,7 @@
SUPPORTS_MACCATALYST = NO;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
- SWIFT_ACTIVE_COMPILATION_CONDITIONS = WITH_QEMU_TCI;
+ SWIFT_ACTIVE_COMPILATION_CONDITIONS = "WITH_QEMU_TCI WITH_SOLO_VM $(inherited)";
SWIFT_OBJC_BRIDGING_HEADER = "Services/Swift-Bridging-Header.h";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2,7";
@@ -4149,6 +4901,73 @@
};
name = Release;
};
+ CEF7F6D12AEEDCC400E34952 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+ CLANG_ENABLE_MODULES = YES;
+ CODE_SIGN_IDENTITY = "$(CODE_SIGN_IDENTITY_IOS:default=Apple Development)";
+ ENABLE_BITCODE = NO;
+ ENABLE_PREVIEWS = YES;
+ GCC_PREPROCESSOR_DEFINITIONS = (
+ "WITH_REMOTE=1",
+ "$(inherited)",
+ );
+ INFOPLIST_FILE = "Platform/iOS/Info-Remote.plist";
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ PLATFORM_SUFFIX = "-TCI";
+ PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_PREFIX:default=com.utmapp).UTM-Remote";
+ PRODUCT_MODULE_NAME = UTM;
+ PRODUCT_NAME = "UTM Remote";
+ SUPPORTED_PLATFORMS = "iphoneos iphonesimulator xros xrsimulator";
+ SUPPORTS_MACCATALYST = NO;
+ SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
+ SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
+ SWIFT_ACTIVE_COMPILATION_CONDITIONS = "WITH_REMOTE $(inherited)";
+ SWIFT_OBJC_BRIDGING_HEADER = "Services/Swift-Bridging-Header.h";
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2,7";
+ };
+ name = Debug;
+ };
+ CEF7F6D22AEEDCC400E34952 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+ CLANG_ENABLE_MODULES = YES;
+ CODE_SIGN_IDENTITY = "$(CODE_SIGN_IDENTITY_IOS:default=Apple Development)";
+ ENABLE_BITCODE = NO;
+ ENABLE_PREVIEWS = YES;
+ GCC_PREPROCESSOR_DEFINITIONS = (
+ "WITH_REMOTE=1",
+ "$(inherited)",
+ );
+ INFOPLIST_FILE = "Platform/iOS/Info-Remote.plist";
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ PLATFORM_SUFFIX = "-TCI";
+ PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_PREFIX:default=com.utmapp).UTM-Remote";
+ PRODUCT_MODULE_NAME = UTM;
+ PRODUCT_NAME = "UTM Remote";
+ SUPPORTED_PLATFORMS = "iphoneos iphonesimulator xros xrsimulator";
+ SUPPORTS_MACCATALYST = NO;
+ SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
+ SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
+ SWIFT_ACTIVE_COMPILATION_CONDITIONS = "WITH_REMOTE $(inherited)";
+ SWIFT_OBJC_BRIDGING_HEADER = "Services/Swift-Bridging-Header.h";
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2,7";
+ };
+ name = Release;
+ };
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
@@ -4206,7 +5025,7 @@
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
- CEA45FB6263519B5002FA97D /* Build configuration list for PBXNativeTarget "iOS-TCI" */ = {
+ CEA45FB6263519B5002FA97D /* Build configuration list for PBXNativeTarget "iOS-SE" */ = {
isa = XCConfigurationList;
buildConfigurations = (
CEA45FB7263519B5002FA97D /* Debug */,
@@ -4224,6 +5043,15 @@
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
+ CEF7F6D02AEEDCC400E34952 /* Build configuration list for PBXNativeTarget "iOS-Remote" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ CEF7F6D12AEEDCC400E34952 /* Debug */,
+ CEF7F6D22AEEDCC400E34952 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
/* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */
@@ -4245,9 +5073,9 @@
};
848F71E4277A2466006A0240 /* XCRemoteSwiftPackageReference "SwiftTerm" */ = {
isa = XCRemoteSwiftPackageReference;
- repositoryURL = "https://github.com/osy/SwiftTerm.git";
+ repositoryURL = "https://github.com/migueldeicaza/SwiftTerm.git";
requirement = {
- branch = visionos;
+ branch = main;
kind = branch;
};
};
@@ -4299,6 +5127,14 @@
minimumVersion = 1.5.3;
};
};
+ CE89CB0C2B8B1B49006B2CC2 /* XCRemoteSwiftPackageReference "VisionKeyboardKit" */ = {
+ isa = XCRemoteSwiftPackageReference;
+ repositoryURL = "https://github.com/utmapp/VisionKeyboardKit.git";
+ requirement = {
+ branch = main;
+ kind = branch;
+ };
+ };
CE93759724BB821F0074066F /* XCRemoteSwiftPackageReference "IQKeyboardManager" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/hackiftekhar/IQKeyboardManager.git";
@@ -4307,6 +5143,14 @@
version = 6.5.6;
};
};
+ CE9B15342B11A491003A32DD /* XCRemoteSwiftPackageReference "SwiftConnect" */ = {
+ isa = XCRemoteSwiftPackageReference;
+ repositoryURL = "https://github.com/utmapp/SwiftConnect";
+ requirement = {
+ branch = main;
+ kind = branch;
+ };
+ };
CEA45E21263519B5002FA97D /* XCRemoteSwiftPackageReference "swift-log" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/apple/swift-log";
@@ -4323,6 +5167,62 @@
minimumVersion = 6.5.6;
};
};
+ CEDD11BF2B7C74D7004DDAC6 /* XCRemoteSwiftPackageReference "SwiftPortmap" */ = {
+ isa = XCRemoteSwiftPackageReference;
+ repositoryURL = "https://github.com/osy/SwiftPortmap.git";
+ requirement = {
+ branch = main;
+ kind = branch;
+ };
+ };
+ CEF7F5852AEEDCC400E34952 /* XCRemoteSwiftPackageReference "swift-log" */ = {
+ isa = XCRemoteSwiftPackageReference;
+ repositoryURL = "https://github.com/apple/swift-log";
+ requirement = {
+ kind = upToNextMajorVersion;
+ minimumVersion = 1.5.3;
+ };
+ };
+ CEF7F5872AEEDCC400E34952 /* XCRemoteSwiftPackageReference "IQKeyboardManager" */ = {
+ isa = XCRemoteSwiftPackageReference;
+ repositoryURL = "https://github.com/hackiftekhar/IQKeyboardManager.git";
+ requirement = {
+ kind = exactVersion;
+ version = 6.5.6;
+ };
+ };
+ CEF7F5892AEEDCC400E34952 /* XCRemoteSwiftPackageReference "ZIPFoundation" */ = {
+ isa = XCRemoteSwiftPackageReference;
+ repositoryURL = "https://github.com/weichsel/ZIPFoundation.git";
+ requirement = {
+ kind = upToNextMajorVersion;
+ minimumVersion = 0.9.17;
+ };
+ };
+ CEF7F58F2AEEDCC400E34952 /* XCRemoteSwiftPackageReference "SwiftTerm" */ = {
+ isa = XCRemoteSwiftPackageReference;
+ repositoryURL = "https://github.com/migueldeicaza/SwiftTerm.git";
+ requirement = {
+ branch = main;
+ kind = branch;
+ };
+ };
+ CEF7F5912AEEDCC400E34952 /* XCRemoteSwiftPackageReference "swiftui-visual-effects" */ = {
+ isa = XCRemoteSwiftPackageReference;
+ repositoryURL = "https://github.com/lucasbrown/swiftui-visual-effects.git";
+ requirement = {
+ kind = exactVersion;
+ version = 1.0.3;
+ };
+ };
+ CEF7F5932AEEDCC400E34952 /* XCRemoteSwiftPackageReference "InAppSettingsKit" */ = {
+ isa = XCRemoteSwiftPackageReference;
+ repositoryURL = "https://github.com/futuretap/InAppSettingsKit.git";
+ requirement = {
+ kind = upToNextMajorVersion;
+ minimumVersion = 3.0.0;
+ };
+ };
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
@@ -4440,11 +5340,46 @@
package = CE020BA524AEDEF000B44AB6 /* XCRemoteSwiftPackageReference "swift-log" */;
productName = Logging;
};
+ CE89CB0D2B8B1B5A006B2CC2 /* VisionKeyboardKit */ = {
+ isa = XCSwiftPackageProductDependency;
+ package = CE89CB0C2B8B1B49006B2CC2 /* XCRemoteSwiftPackageReference "VisionKeyboardKit" */;
+ productName = VisionKeyboardKit;
+ };
+ CE89CB0F2B8B1B6A006B2CC2 /* VisionKeyboardKit */ = {
+ isa = XCSwiftPackageProductDependency;
+ package = CE89CB0C2B8B1B49006B2CC2 /* XCRemoteSwiftPackageReference "VisionKeyboardKit" */;
+ productName = VisionKeyboardKit;
+ };
+ CE89CB112B8B1B7A006B2CC2 /* VisionKeyboardKit */ = {
+ isa = XCSwiftPackageProductDependency;
+ package = CE89CB0C2B8B1B49006B2CC2 /* XCRemoteSwiftPackageReference "VisionKeyboardKit" */;
+ productName = VisionKeyboardKit;
+ };
CE93759824BB821F0074066F /* IQKeyboardManagerSwift */ = {
isa = XCSwiftPackageProductDependency;
package = CE93759724BB821F0074066F /* XCRemoteSwiftPackageReference "IQKeyboardManager" */;
productName = IQKeyboardManagerSwift;
};
+ CE9B15352B11A491003A32DD /* SwiftConnect */ = {
+ isa = XCSwiftPackageProductDependency;
+ package = CE9B15342B11A491003A32DD /* XCRemoteSwiftPackageReference "SwiftConnect" */;
+ productName = SwiftConnect;
+ };
+ CE9B15372B11A4A7003A32DD /* SwiftConnect */ = {
+ isa = XCSwiftPackageProductDependency;
+ package = CE9B15342B11A491003A32DD /* XCRemoteSwiftPackageReference "SwiftConnect" */;
+ productName = SwiftConnect;
+ };
+ CE9B15392B11A4AE003A32DD /* SwiftConnect */ = {
+ isa = XCSwiftPackageProductDependency;
+ package = CE9B15342B11A491003A32DD /* XCRemoteSwiftPackageReference "SwiftConnect" */;
+ productName = SwiftConnect;
+ };
+ CE9B153B2B11A4B4003A32DD /* SwiftConnect */ = {
+ isa = XCSwiftPackageProductDependency;
+ package = CE9B15342B11A491003A32DD /* XCRemoteSwiftPackageReference "SwiftConnect" */;
+ productName = SwiftConnect;
+ };
CEA45E20263519B5002FA97D /* Logging */ = {
isa = XCSwiftPackageProductDependency;
package = CEA45E21263519B5002FA97D /* XCRemoteSwiftPackageReference "swift-log" */;
@@ -4455,6 +5390,46 @@
package = CEA45E23263519B5002FA97D /* XCRemoteSwiftPackageReference "IQKeyboardManager" */;
productName = IQKeyboardManagerSwift;
};
+ CEDD11C02B7C74D7004DDAC6 /* SwiftPortmap */ = {
+ isa = XCSwiftPackageProductDependency;
+ package = CEDD11BF2B7C74D7004DDAC6 /* XCRemoteSwiftPackageReference "SwiftPortmap" */;
+ productName = SwiftPortmap;
+ };
+ CEF7F5842AEEDCC400E34952 /* Logging */ = {
+ isa = XCSwiftPackageProductDependency;
+ package = CEF7F5852AEEDCC400E34952 /* XCRemoteSwiftPackageReference "swift-log" */;
+ productName = Logging;
+ };
+ CEF7F5862AEEDCC400E34952 /* IQKeyboardManagerSwift */ = {
+ isa = XCSwiftPackageProductDependency;
+ package = CEF7F5872AEEDCC400E34952 /* XCRemoteSwiftPackageReference "IQKeyboardManager" */;
+ productName = IQKeyboardManagerSwift;
+ };
+ CEF7F5882AEEDCC400E34952 /* ZIPFoundation */ = {
+ isa = XCSwiftPackageProductDependency;
+ package = CEF7F5892AEEDCC400E34952 /* XCRemoteSwiftPackageReference "ZIPFoundation" */;
+ productName = ZIPFoundation;
+ };
+ CEF7F58E2AEEDCC400E34952 /* SwiftTerm */ = {
+ isa = XCSwiftPackageProductDependency;
+ package = CEF7F58F2AEEDCC400E34952 /* XCRemoteSwiftPackageReference "SwiftTerm" */;
+ productName = SwiftTerm;
+ };
+ CEF7F5902AEEDCC400E34952 /* SwiftUIVisualEffects */ = {
+ isa = XCSwiftPackageProductDependency;
+ package = CEF7F5912AEEDCC400E34952 /* XCRemoteSwiftPackageReference "swiftui-visual-effects" */;
+ productName = SwiftUIVisualEffects;
+ };
+ CEF7F5922AEEDCC400E34952 /* InAppSettingsKit */ = {
+ isa = XCSwiftPackageProductDependency;
+ package = CEF7F5932AEEDCC400E34952 /* XCRemoteSwiftPackageReference "InAppSettingsKit" */;
+ productName = InAppSettingsKit;
+ };
+ CEF7F6D52AEEEF7D00E34952 /* CocoaSpiceNoUsb */ = {
+ isa = XCSwiftPackageProductDependency;
+ package = 84B36D1C27B3261E00C22685 /* XCRemoteSwiftPackageReference "CocoaSpice" */;
+ productName = CocoaSpiceNoUsb;
+ };
/* End XCSwiftPackageProductDependency section */
};
rootObject = CE550BC1225947990063E575 /* Project object */;
diff --git a/UTM.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/UTM.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
index 637d86472..3d420c8b7 100644
--- a/UTM.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
+++ b/UTM.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
@@ -15,7 +15,16 @@
"location" : "https://github.com/utmapp/CocoaSpice.git",
"state" : {
"branch" : "visionos",
- "revision" : "4529c9686259e8d1e94d6253ad2e3a563fd1498d"
+ "revision" : "9fd682e0f78c884036609d4a19db2cfb3ed50c33"
+ }
+ },
+ {
+ "identity" : "cod",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/saagarjha/Cod.git",
+ "state" : {
+ "branch" : "main",
+ "revision" : "c359a08accfb49662a17cdfc5e333c7b4e5c2c56"
}
},
{
@@ -63,13 +72,31 @@
"version" : "1.5.3"
}
},
+ {
+ "identity" : "swiftconnect",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/utmapp/SwiftConnect",
+ "state" : {
+ "branch" : "main",
+ "revision" : "af855e47ca222da163cc7f4f185230f36ba8694a"
+ }
+ },
+ {
+ "identity" : "swiftportmap",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/osy/SwiftPortmap.git",
+ "state" : {
+ "branch" : "main",
+ "revision" : "72782141ab6f6f6db58bd16bac96d4e7ce901e9a"
+ }
+ },
{
"identity" : "swiftterm",
"kind" : "remoteSourceControl",
- "location" : "https://github.com/osy/SwiftTerm.git",
+ "location" : "https://github.com/migueldeicaza/SwiftTerm.git",
"state" : {
- "branch" : "visionos",
- "revision" : "8b0900a4c516eb8c87813f11e797f349e7fca014"
+ "branch" : "main",
+ "revision" : "ea0f681b25c8385b4a5a48d435e61d11392216e0"
}
},
{
@@ -81,6 +108,15 @@
"version" : "1.0.3"
}
},
+ {
+ "identity" : "visionkeyboardkit",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/utmapp/VisionKeyboardKit.git",
+ "state" : {
+ "branch" : "main",
+ "revision" : "0804e4d64267acc8d08fb23160f5b6ac6134414f"
+ }
+ },
{
"identity" : "zipfoundation",
"kind" : "remoteSourceControl",
diff --git a/patches/sources b/patches/sources
index 123dd9c08..b8a0bff52 100644
--- a/patches/sources
+++ b/patches/sources
@@ -37,7 +37,7 @@ SPICE_CLIENT_SRC="https://www.spice-space.org/download/gtk/spice-gtk-0.40.tar.xz
# Source files for GPU acceleration
WEBKIT_REPO="https://github.com/utmapp/WebKit.git"
-WEBKIT_COMMIT="9ce1f852e15f6f322cee0ce727c8fd73657705c9"
+WEBKIT_COMMIT="b5f22a32a49682059749b2cccac06231a20c1387"
WEBKIT_SUBDIRS="Source/ThirdParty/ANGLE Configurations Tools/ccache"
EPOXY_REPO="https://github.com/utmapp/libepoxy.git"
EPOXY_COMMIT="266d2290a437c655f7419e85af06bfbb73a720c4"
@@ -46,4 +46,4 @@ VIRGLRENDERER_COMMIT="dc039d9ecd74fc671a85bfbe7c4e4bc552b7b855"
# Decompiled Hypervisor for iOS
HYPERVISOR_REPO="https://github.com/utmapp/Hypervisor.git"
-HYPERVISOR_COMMIT="b4eb0e00c03692016944fad3e8e3a6613839912e"
+HYPERVISOR_COMMIT="bcec1a8b0cb3b07ecd0f81b33f26fa3048ffd307"
diff --git a/scripts/build_dependencies.sh b/scripts/build_dependencies.sh
index 75b8d66b2..5bb3e059d 100755
--- a/scripts/build_dependencies.sh
+++ b/scripts/build_dependencies.sh
@@ -409,6 +409,8 @@ build_angle () {
pwd="$(pwd)"
cd "$BUILD_DIR/WebKit.git/Source/ThirdParty/ANGLE"
xcodebuild archive -archivePath "ANGLE" -scheme "ANGLE" -sdk $SDK -arch $ARCH -configuration Release WEBCORE_LIBRARY_DIR="/usr/local/lib" IPHONEOS_DEPLOYMENT_TARGET="14.0" MACOSX_DEPLOYMENT_TARGET="11.0" XROS_DEPLOYMENT_TARGET="1.0"
+ # strip broken entitlements from signature
+ find "ANGLE.xcarchive/Products/usr/local/lib/" -name '*.dylib' -exec codesign -fs - \{\} \;
rsync -a "ANGLE.xcarchive/Products/usr/local/lib/" "$PREFIX/lib"
rsync -a "include/" "$PREFIX/include"
cd "$pwd"
diff --git a/scripts/build_utm.sh b/scripts/build_utm.sh
index 191e77a72..6ea37890c 100755
--- a/scripts/build_utm.sh
+++ b/scripts/build_utm.sh
@@ -7,11 +7,12 @@ command -v realpath >/dev/null 2>&1 || realpath() {
BASEDIR="$(dirname "$(realpath $0)")"
usage () {
- echo "Usage: $(basename $0) [-t teamid] [-p platform] [-a architecture] [-t targetversion] [-o output]"
+ echo "Usage: $(basename $0) [-t teamid] [-p platform] [-s scheme] [-a architecture] [-t targetversion] [-o output]"
echo ""
echo " -t teamid Team Identifier for app groups. Optional for iOS. Required for macOS."
- echo " -p platform Target platform. Default ios. [ios|ios_simulator|ios-tci|ios_simulator-tci|macos|visionos|visionos_simulator]"
- echo " -a architecture Target architecture. Default arm64. [armv7|armv7s|arm64|i386|x86_64]"
+ echo " -k sdk Target SDK. Default iphoneos. [iphoneos|iphonesimulator|xros|xrsimulator|macosx]"
+ echo " -s scheme Target scheme. Default iOS/macOS depending on platform. [iOS|iOS-TCI|iOS-Remote|macOS]"
+ echo " -a architecture Target architecture. Default arm64. [arm64|x86_64]"
echo " -o output Output archive path. Default is current directory."
echo ""
exit 1
@@ -20,9 +21,8 @@ usage () {
PRODUCT_BUNDLE_PREFIX="com.utmapp"
TEAM_IDENTIFIER=
ARCH=arm64
-PLATFORM=ios
OUTPUT=$PWD
-SDK=
+SDK=iphoneos
SCHEME=
while [ "x$1" != "x" ]; do
@@ -35,8 +35,12 @@ while [ "x$1" != "x" ]; do
ARCH=$2
shift
;;
- -p )
- PLATFORM=$2
+ -k )
+ SDK=$2
+ shift
+ ;;
+ -s )
+ SCHEME=$2
shift
;;
-o )
@@ -50,39 +54,14 @@ while [ "x$1" != "x" ]; do
shift
done
-case $PLATFORM in
-*-tci )
- SCHEME="iOS-TCI"
- ;;
-ios* | visionos* )
- SCHEME="iOS"
- ;;
+case $SDK in
macos )
SCHEME="macOS"
;;
* )
- usage
- ;;
-esac
-
-case $PLATFORM in
-visionos_simulator* )
- SDK=xrsimulator
- ;;
-visionos* )
- SDK=xros
- ;;
-ios_simulator* )
- SDK=iphonesimulator
- ;;
-ios* )
- SDK=iphoneos
- ;;
-macos )
- SDK=macosx
- ;;
-* )
- usage
+ if [ -z "$SCHEME" ]; then
+ SCHEME="iOS"
+ fi
;;
esac
@@ -94,8 +73,7 @@ fi
xcodebuild archive -archivePath "$OUTPUT" -scheme "$SCHEME" -sdk "$SDK" $ARCH_ARGS -configuration Release CODE_SIGNING_ALLOWED=NO $TEAM_IDENTIFIER_PREFIX
BUILT_PATH=$(find $OUTPUT.xcarchive -name '*.app' -type d | head -1)
# Only retain the target architecture to address < iOS 15 crash & save disk space
-case $PLATFORM in
-ios | ios-tci )
+if [ "$SDK" == "iphoneos" ]; then
find "$BUILT_PATH" -type f -path '*/Frameworks/*.dylib' | while read FILE; do
if [[ $(lipo -info "$FILE") =~ "Architectures in the fat file" ]]; then
lipo -thin $ARCH "$FILE" -output "$FILE"
@@ -107,10 +85,9 @@ ios | ios-tci )
lipo -thin $ARCH "$FILE" -output "$FILE"
fi
done
- ;;
-esac
+fi
find "$BUILT_PATH" -type d -path '*/Frameworks/*.framework' -exec codesign --force --sign - --timestamp=none \{\} \;
-if [ "$PLATFORM" == "macos" ]; then
+if [ "$SDK" == "macosx" ]; then
# always build with vm entitlements, package_mac.sh can strip it later
# this way we can import into Xcode and re-sign from there
UTM_ENTITLEMENTS="/tmp/utm.$$.entitlements"
diff --git a/scripts/package.sh b/scripts/package.sh
index 9c48bbe46..73b24931a 100755
--- a/scripts/package.sh
+++ b/scripts/package.sh
@@ -12,7 +12,8 @@ usage() {
echo " MODE is one of:"
echo " deb (Cydia DEB)"
echo " ipa (unsigned IPA of full build with all entitlements)"
- echo " ipa-se (unsigned IPA of TCI build)"
+ echo " ipa-se (unsigned IPA of SE build)"
+ echo " ipa-remote (unsigned IPA of Remote build)"
echo " ipa-hv (unsigned IPA of full build without JIT entitlement)"
echo " ipa-signed (developer signed IPA with valid PROFILE_NAME and TEAM_ID)"
echo " inputXcarchive is path to UTM.xcarchive"
@@ -42,6 +43,11 @@ ipa-se )
BUNDLE_ID="com.utmapp.UTM-SE"
INPUT_APP="$INPUT/Products/Applications/UTM SE.app"
;;
+ipa-remote )
+ NAME="UTM Remote"
+ BUNDLE_ID="com.utmapp.UTM-Remote"
+ INPUT_APP="$INPUT/Products/Applications/UTM Remote.app"
+ ;;
* )
usage
;;
@@ -298,7 +304,7 @@ EOL
create_fake_ipa "$NAME" "$BUNDLE_ID" "$INPUT" "$OUTPUT" "$FAKEENT"
rm "$FAKEENT"
;;
-ipa-se )
+ipa-se | ipa-remote )
FAKEENT="/tmp/fakeent.$$.plist"
cat >"$FAKEENT" <
diff --git a/scripts/package_mac.sh b/scripts/package_mac.sh
index beb6fa0f5..7a68b2435 100755
--- a/scripts/package_mac.sh
+++ b/scripts/package_mac.sh
@@ -115,8 +115,7 @@ if [ "$MODE" == "app-store" ]; then
cp "$SIGNED/UTM.pkg" "$OUTPUT/UTM.pkg"
else
rm -f "$OUTPUT/UTM.dmg"
- command -v appdmg >/dev/null 2>&1
- if [ $? -eq 0 ]; then
+ if command -v appdmg >/dev/null 2>&1; then
RESOURCES="/tmp/resources.$$"
cp -r "$BASEDIR/resources" "$RESOURCES"
sed -i '' "s/\/tmp\/signed\/UTM.app/\/tmp\/signed.$$\/UTM.app/g" "$RESOURCES/appdmg.json"