Browse Source

Merge branch 'main' into utm-import

osy 9 months ago
parent
commit
f11cda7a3b
50 changed files with 1484 additions and 1238 deletions
  1. 21 9
      Configuration/UTMAppleConfiguration.swift
  2. 4 0
      Configuration/UTMAppleConfigurationSystem.swift
  3. 1 1
      Configuration/UTMAppleConfigurationVirtualization.swift
  4. 1 1
      Configuration/UTMQemuConfiguration+Arguments.swift
  5. 3 0
      Configuration/UTMQemuConfigurationQEMU.swift
  6. 1 1
      Documentation/MacDevelopment.md
  7. 33 33
      Documentation/TetheredLaunch.zh-HK.md
  8. 12 12
      Documentation/TetheredLaunch.zh-Hans.md
  9. 9 2
      Platform/Shared/BusyOverlay.swift
  10. 3 25
      Platform/Shared/ContentView.swift
  11. 9 0
      Platform/Shared/VMContextMenuModifier.swift
  12. 12 1
      Platform/Shared/VMWizardState.swift
  13. 74 19
      Platform/UTMData.swift
  14. 68 0
      Platform/UTMDownloadMacSupportToolsTask.swift
  15. 3 3
      Platform/iOS/Display/zh-HK.lproj/VMDisplayMetalViewInputAccessory.strings
  16. 3 3
      Platform/iOS/zh-HK.lproj/Info-RemotePlist.strings
  17. 6 6
      Platform/iOS/zh-HK.lproj/InfoPlist.strings
  18. 1 1
      Platform/iOS/zh-Hans.lproj/Info-Remote-InfoPlist.strings
  19. 1 1
      Platform/iOS/zh-Hans.lproj/Info-RemotePlist.strings
  20. 5 5
      Platform/iOS/zh-Hans.lproj/InfoPlist.strings
  21. 72 4
      Platform/macOS/Display/VMDisplayAppleDisplayWindowController.swift
  22. 145 1
      Platform/macOS/Display/VMDisplayAppleWindowController.swift
  23. 41 3
      Platform/macOS/Display/VMDisplayQemuMetalWindowController.swift
  24. 1 1
      Platform/macOS/Display/zh-Hans.lproj/VMDisplayWindow.strings
  25. 1 1
      Platform/macOS/UTMDataExtension.swift
  26. 23 5
      Platform/macOS/VMAppleRemovableDrivesView.swift
  27. 2 1
      Platform/macOS/VMConfigAppleVirtualizationView.swift
  28. 2 2
      Platform/macOS/zh-HK.lproj/InfoPlist.strings
  29. 1 1
      Platform/macOS/zh-Hans.lproj/InfoPlist.strings
  30. 163 163
      Platform/zh-HK.lproj/Localizable.strings
  31. 96 96
      Platform/zh-Hans.lproj/Localizable.strings
  32. 1 1
      QEMUHelper/zh-Hans.lproj/InfoPlist.strings
  33. 1 1
      README.zh-Hans.md
  34. 9 0
      Scripting/UTM.sdef
  35. 1 0
      Scripting/UTMScripting.swift
  36. 31 0
      Scripting/UTMScriptingExportCommand.swift
  37. 10 0
      Scripting/UTMScriptingVirtualMachineImpl.swift
  38. 168 8
      Services/UTMAppleVirtualMachine.swift
  39. 44 2
      Services/UTMRegistryEntry.swift
  40. 44 18
      UTM.xcodeproj/project.pbxproj
  41. 0 114
      patches/libsoup-2.74.2.patch
  42. 255 0
      patches/libsoup-3.6.0.patch
  43. 0 135
      patches/phodav-2.5.patch
  44. 27 0
      patches/phodav-3.0.patch
  45. 28 0
      patches/qemu-9.1.0-utm.patch
  46. 6 6
      patches/sources
  47. 14 548
      patches/spice-gtk-0.42.patch
  48. 5 3
      scripts/build_dependencies.sh
  49. 22 0
      utmctl/UTMCtl.swift
  50. 1 1
      utmctl/utmctl-unsigned.entitlements

+ 21 - 9
Configuration/UTMAppleConfiguration.swift

@@ -36,7 +36,10 @@ final class UTMAppleConfiguration: UTMConfiguration {
     @Published private var _networks: [UTMAppleConfigurationNetwork] = [.init()]
     
     @Published private var _serials: [UTMAppleConfigurationSerial] = []
-    
+
+    /// Set to true to request guest tools install. Not saved.
+    @Published var isGuestToolsInstallRequested: Bool = false
+
     var backend: UTMBackend {
         .apple
     }
@@ -251,13 +254,7 @@ extension UTMAppleConfiguration {
         let vzconfig = VZVirtualMachineConfiguration()
         try system.fillVZConfiguration(vzconfig)
         if #available(macOS 12, *), !sharedDirectories.isEmpty {
-            let tag: String
-            if #available(macOS 13, *), system.boot.operatingSystem == .macOS {
-                tag = VZVirtioFileSystemDeviceConfiguration.macOSGuestAutomountTag
-            } else {
-                tag = "share"
-            }
-            let fsConfig = VZVirtioFileSystemDeviceConfiguration(tag: tag)
+            let fsConfig = VZVirtioFileSystemDeviceConfiguration(tag: shareDirectoryTag)
             fsConfig.share = UTMAppleConfigurationSharedDirectory.makeDirectoryShare(from: sharedDirectories)
             vzconfig.directorySharingDevices.append(fsConfig)
         } else if !sharedDirectories.isEmpty {
@@ -269,7 +266,11 @@ extension UTMAppleConfiguration {
                     return nil
                 }
                 if #available(macOS 13, *), drive.isExternal {
-                    return VZUSBMassStorageDeviceConfiguration(attachment: attachment)
+                    if #available(macOS 15, *) {
+                        return nil // we will handle removable drives in `UTMAppleVirtualMachine`
+                    } else {
+                        return VZUSBMassStorageDeviceConfiguration(attachment: attachment)
+                    }
                 } else if #available(macOS 14, *), drive.isNvme, system.boot.operatingSystem == .linux {
                     return VZNVMExpressControllerDeviceConfiguration(attachment: attachment)
                 } else {
@@ -303,8 +304,19 @@ extension UTMAppleConfiguration {
         } else if system.boot.operatingSystem != .macOS && !displays.isEmpty {
             throw UTMAppleConfigurationError.featureNotSupported
         }
+        if #available(macOS 15, *) {
+            vzconfig.usbControllers = [VZXHCIControllerConfiguration()]
+        }
         return vzconfig
     }
+
+    var shareDirectoryTag: String {
+        if #available(macOS 13, *), system.boot.operatingSystem == .macOS {
+            return VZVirtioFileSystemDeviceConfiguration.macOSGuestAutomountTag
+        } else {
+            return "share"
+        }
+    }
 }
 
 // MARK: - Saving data

+ 4 - 0
Configuration/UTMAppleConfigurationSystem.swift

@@ -67,6 +67,10 @@ struct UTMAppleConfigurationSystem: Codable {
         boot = try values.decode(UTMAppleConfigurationBoot.self, forKey: .boot)
         macPlatform = try values.decodeIfPresent(UTMAppleConfigurationMacPlatform.self, forKey: .macPlatform)
         genericPlatform = try values.decodeIfPresent(UTMAppleConfigurationGenericPlatform.self, forKey: .genericPlatform)
+        if boot.operatingSystem == .linux && genericPlatform == nil {
+            // fix a bug where this was not created
+            genericPlatform = UTMAppleConfigurationGenericPlatform()
+        }
     }
     
     func encode(to encoder: Encoder) throws {

+ 1 - 1
Configuration/UTMAppleConfigurationVirtualization.swift

@@ -191,7 +191,7 @@ extension UTMAppleConfigurationVirtualization {
                 throw UTMAppleConfigurationError.rosettaNotSupported
             }
             #endif
-            if hasClipboardSharing && !isMacOSGuest {
+            if hasClipboardSharing {
                 let spiceClipboardAgent = VZSpiceAgentPortAttachment()
                 spiceClipboardAgent.sharesClipboard = true
                 let consolePort = VZVirtioConsolePortConfiguration()

+ 1 - 1
Configuration/UTMQemuConfiguration+Arguments.swift

@@ -644,7 +644,7 @@ import Virtualization // for getting network interfaces
             f()
         } else if drive.interface == .scsi {
             var bus = "scsi"
-            if system.architecture != .sparc && system.architecture != .sparc64 {
+            if system.architecture != .sparc && system.architecture != .sparc64 && system.architecture != .m68k {
                 bus = "scsi0"
                 if busindex == 0 {
                     f("-device")

+ 3 - 0
Configuration/UTMQemuConfigurationQEMU.swift

@@ -15,6 +15,7 @@
 //
 
 import Foundation
+import System
 
 /// Tweaks and advanced QEMU settings.
 struct UTMQemuConfigurationQEMU: Codable {
@@ -189,6 +190,8 @@ extension UTMQemuConfigurationQEMU {
             if !fileManager.fileExists(atPath: varsURL.path) {
                 try await Task.detached {
                     try FileManager.default.copyItem(at: templateVarsURL, to: varsURL)
+                    let permissions: FilePermissions = [.ownerReadWrite, .groupRead, .otherRead]
+                    try FileManager.default.setAttributes([.posixPermissions: permissions.rawValue], ofItemAtPath: varsURL.path)
                 }.value
             }
             efiVarsURL = varsURL

+ 1 - 1
Documentation/MacDevelopment.md

@@ -16,7 +16,7 @@ git submodule update --init --recursive
 
 ## Dependencies
 
-The easy way is to get the prebuilt dependences from [GitHub Actions][1]. Pick the latest release and download all of the `Sysroot-macos-*` artifacts. You need to be logged in to GitHub to download artifacts. If you only intend to run locally, it is alright to just download the sysroot for your architecture. After downloading the prebuilt artifacts of your choice, extract them to the root directory where you cloned the repository.
+The easy way is to get the prebuilt dependencies from [GitHub Actions][1]. Pick the latest release and download all of the `Sysroot-macos-*` artifacts. You need to be logged in to GitHub to download artifacts. If you only intend to run locally, it is alright to just download the sysroot for your architecture. After downloading the prebuilt artifacts of your choice, extract them to the root directory where you cloned the repository.
 
 To build UTM, make sure you have the latest version of Xcode installed.
 

+ 33 - 33
Documentation/TetheredLaunch.zh-HK.md

@@ -1,53 +1,53 @@
-# 不完全啟動
+# 捆綁式啟動
 
- 在iOS14中,Apple[修補][1]了我們用來使JIT工作的“把戲”。 因此,下一個最佳的解決方案所涉及的範圍更廣。 這只支援非越獄設備。 如果你越獄了,你不需要這樣做。
+在 iOS 14 當中,Apple [修複][1]了我們之前令 JIT 工作的“蠱惑招”。因此,下一個最佳的變通方法涉及更多。這只限用於未越獄的裝置。如你已經越獄,就無需這樣做。
 
- ## 前條件
+## 先決條件
 
- * Xcode
- * [最新的正用版IPA][3]
- * [iOS App Signer][4]
- * [Homebrew][2]
- * [ios-deploy][5] (`brew install ios-deploy`)
+* Xcode
+* [最新版本的 IPA][3]
+* [iOS App Signer][4]
+* [Homebrew][2]
+* [ios-deploy][5] (`brew install ios-deploy`)
 
- ## 簽字
+## 簽署
 
- 安裝並按照[iOS App Signer][4]的說明進行操作。 請確保您的簽字證書和配置文件匹配。 選擇UTM.ipa正式版作為輸入文件並且按下開始
+安裝並依照 [iOS App Signer][4] 的說明執行操作。確保你的簽署證書與配置檔案匹配。選擇 UTM.ipa 發行版本作為輸入檔案,然後按一下「開始」
 
- 將已簽字的IPA保存為`UTM-signed.ipa`,過程完成後將`UTM-signed.ipa`重命名為`UTM-signed.zip`並且打開ZIP文件。  macOS會將文件提取至名為`Payload/`的新目錄
+將已經簽署的 IPA 儲存為 `UTM-signed.ipa`,完成程序之後,將 `UTM-signed.ipa` 重新命名為`UTM-signed.zip`,並且開啟 ZIP 檔案。macOS 應將檔案解壓縮至名稱為 `Payload/` 的新目錄當中
 
- ## 部署
+## 部署
 
- 要部署UTM,連接你的設備然後在終端中運行:
+如要部署 UTM,連接你的裝置並在終端機中執行:
 
- ```sh
- ios-deploy --bundle /path/to/Payload/UTM.app
- ```
+```sh
+ios-deploy --bundle /path/to/Payload/UTM.app
+```
 
- (提示:你可以把 `Payload/UTM.app` 拖放進終端來自動填充目錄。)
+(貼士:你可以拖放 `Payload/UTM.app` 至終端機以自動填充目錄。)
 
- ## 啟動
+## 啟動
 
- 當你每次希望啟動UTM時,都需要運行以下命令。  (你無法在iOS14中從主頁面正常啟動UTM否則它無法正常運行!)
+如你每次希望啟動 UTM,都需要執行以下內容。(在 iOS 14 當中,不應該透過主畫面啟動 UTM,否則它將無法正常工作!)
 
- ```sh
- ios-deploy --justlaunch --noinstall --bundle /path/to/Payload/UTM.app
- ```
+```sh
+ios-deploy --justlaunch --noinstall --bundle /path/to/Payload/UTM.app
+```
 
- (提示:如果您打開Xcode並轉到Window->Devices and Simulators並找到您的設備,那麼您可以選中“Connect via network”(通過網路連接)以便在沒有USB連線的情況下部署/啟動。你只 需要解鎖設備並靠近你的電腦。)
+(貼士:如你要開啟 Xcode 並轉到 Window > Devices and Simulators 找到你的裝置,則你可以選中「Connect via network」以便於在無 USB 連線的條件下部署/啟動。你只需要解鎖裝置並令它靠近你的電腦。)
 
- ## 疑難解
+## 疑難
 
- ### 信任問題
+### 信任問題
 
- 如果你看見了消息:`The operation couldn't be completed. Unable to launch xxx because it has an invalid code signature, inadequate entitlements or its profile has not been explicitly trusted by the user.(無法完成操作。無法啟動xxx, 因為它的代碼簽名無效、授權不足或其配置文件未被用戶明確信任。 )`,你需要打開設置-> 通用-> 設備管理,選擇開發者描述文件,然後選擇信任
+如你看到訊息:`The operation couldn't be completed. Unable to launch xxx because it has an invalid code signature, inadequate entitlements or its profile has not been explicitly trusted by the user.`,你需要開啟設定 > 一般 > 裝置管理,選擇「開發者描述檔」,然後選擇「信任」
 
- ### 註冊捆綁標識符失敗
+### 註冊套裝識別碼失敗(Failed to register bundle identifier)
 
- Xcode 可能在嘗試創建簽名配置文件時顯示此消息,您需要更改綁定標識符並重試。
+Xcode 可能在嘗試製作簽名設定檔時顯示此訊息,你需要更改套裝識別碼,然後再試。
 
- [1]: https://github.com/utmapp/UTM/issues/397
- [2]: https://brew.sh
- [3]: https://github.com/utmapp/UTM/releases
- [4]: https://dantheman827.github.io/ios-app-signer/
- [5]: https://github.com/ios-control/ios-deploy
+[1]: https://github.com/utmapp/UTM/issues/397
+[2]: https://brew.sh
+[3]: https://github.com/utmapp/UTM/releases
+[4]: https://dantheman827.github.io/ios-app-signer/
+[5]: https://github.com/ios-control/ios-deploy

+ 12 - 12
Documentation/TetheredLaunch.zh-Hans.md

@@ -1,50 +1,50 @@
-# 不完美启动
+# 捆绑启动
 
-在iOS14中,苹果[修补][1]了我们用来让JIT工作的“把戏”。因此,下一个最佳的解决方案所涉及的范围更广。这只适用于非越狱设备。 如果你越狱了,你不需要这样做
+在 iOS 14 中,Apple [修补][1]了我们用来让 JIT 工作的“把戏”。因此,下一个最佳的解决方案所涉及的范围更广。这一操作只适用于非越狱设备。如果你已经越狱,就不需要这样做了
 
 ## 前置条件
 
 * Xcode
-* [最新的正式版IPA][3]
+* [最新版本的 IPA][3]
 * [iOS App Signer][4]
 * [Homebrew][2]
 * [ios-deploy][5] (`brew install ios-deploy`)
 
 ## 签名
 
-安装并按照[iOS App Signer][4]的说明进行操作。请确保您的签名证书和配置文件匹配。 选择UTM.ipa正式版作为输入文件并且按下开始
+安装并按照 [iOS App Signer][4] 的说明进行操作。确保你的签名证书和配置文件相匹配。选择 UTM.ipa 版本作为输入的文件,然后点击“开始”
 
-将已签名的IPA保存为`UTM-signed.ipa`,过程完成后将`UTM-signed.ipa`重命名为`UTM-signed.zip`并且打开ZIP文件。 macOS会将文件提取至名为`Payload/`的新目录
+将已签名的 IPA 保存为 `UTM-signed.ipa`,完成操作后将 `UTM-signed.ipa` 重命名为 `UTM-signed.zip`,打开 ZIP 文件。 macOS 会将文件提取到名为`Payload/`的新目录中
 
 ## 部署
 
-要部署UTM,连接你的设备然后在终端中运行:
+要部署 UTM,连接你的设备然后在终端中运行:
 
 ```sh
 ios-deploy --bundle /path/to/Payload/UTM.app
 ```
 
-(提示:你可以把 `Payload/UTM.app` 拖放进终端来自动填充目录。)
+(提示:你可以把 `Payload/UTM.app` 拖放进终端来自动填充目录。)
 
 ## 启动
 
-当你每次希望启动UTM时,都需要运行以下命令。 (你无法在iOS14中从主屏幕正常启动UTM否则它无法正常运行!)
+当你每次希望启动 UTM 时,都需要运行如下命令。(不能在 iOS 14 中从主屏幕启动 UTM,否则它将无法正常工作!)
 
 ```sh
 ios-deploy --justlaunch --noinstall --bundle /path/to/Payload/UTM.app
 ```
 
-(提示:如果您打开Xcode并转到Window->Devices and Simulators并找到您的设备,那么您可以选中“Connect via network”(通过网络连接)以便在没有USB电缆的情况下部署/启动。你只需要解锁设备并靠近你的电脑。)
+(提示:如果你打开了 Xcode 并转到窗口(Window)> 设备和模拟器(Devices and Simulators)并找到你的设备,可以勾选“通过网络连接”,以便在没有 USB 电缆的情况下部署/启动。只需要解锁设备并靠近你的电脑即可。)
 
 ## 疑难解答
 
 ### 信任问题
 
-如果你看见了消息:`The operation couldn’t be completed. Unable to launch xxx because it has an invalid code signature, inadequate entitlements or its profile has not been explicitly trusted by the user.(无法完成操作。无法启动xxx,因为它的代码签名无效、授权不足或其配置文件未被用户明确信任。 )`,你需要打开设置 -> 通用 -> 设备管理,选择开发者描述文件,然后选择信任。
+如果你看到了消息 `The operation couldn’t be completed. Unable to launch xxx because it has an invalid code signature, inadequate entitlements or its profile has not been explicitly trusted by the user.(无法完成操作。无法启动 xxx,因为它的代码签名无效,授权不足,或者其配置文件尚未被用户明确信任。)`,你需要打开设置 > 通用 > 设备管理,选择开发者描述文件,然后选择信任。
 
-### 注册捆绑标识符失败
+### 注册捆绑标识符失败(Failed to register bundle identifier)
 
-Xcode 可能在尝试创建签名配置文件时显示此消息,需要更改绑定标识符并重试。
+Xcode 可能在尝试创建签名配置文件时显示此消息,需要更改绑定标识符并重试。
 
 [1]: https://github.com/utmapp/UTM/issues/397
 [2]: https://brew.sh

+ 9 - 2
Platform/Shared/BusyOverlay.swift

@@ -27,8 +27,15 @@ struct BusyOverlay: View {
                 EmptyView()
             }
         }
-        .alert(item: $data.alertMessage) { alertMessage in
-            Alert(title: Text(alertMessage.message))
+        .alert(item: $data.alertItem) { item in
+            switch item {
+            case .downloadUrl(let url):
+                return Alert(title: Text("Download VM"), message: Text("Do you want to download '\(url)'?"), primaryButton: .cancel(), secondaryButton: .default(Text("Download")) {
+                    data.downloadUTMZip(from: url)
+                })
+            case .message(let message):
+                return Alert(title: Text(message))
+            }
         }
     }
 }

+ 3 - 25
Platform/Shared/ContentView.swift

@@ -34,9 +34,7 @@ struct ContentView: View {
     @State private var editMode = false
     @EnvironmentObject private var data: UTMData
     @StateObject private var releaseHelper = UTMReleaseHelper()
-    @State private var newPopupPresented = false
     @State private var openSheetPresented = false
-    @State private var alertItem: AlertItem?
     @Environment(\.openURL) var openURL
     @AppStorage("ServerAutostart") private var isServerAutostart: Bool = false
 
@@ -55,14 +53,6 @@ struct ContentView: View {
         }, content: {
             VMReleaseNotesView(helper: releaseHelper).padding()
         })
-        .alert(item: $alertItem) { item in
-            switch item {
-            case .downloadUrl(let url):
-                return Alert(title: Text("Download VM"), message: Text("Do you want to download '\(url)'?"), primaryButton: .cancel(), secondaryButton: .default(Text("Download")) {
-                    data.downloadUTMZip(from: url)
-                })
-            }
-        }
         .onReceive(NSNotification.ShowReleaseNotes) { _ in
             Task {
                 await releaseHelper.fetchReleaseNotes(force: true)
@@ -148,8 +138,9 @@ struct ContentView: View {
            components.host == "downloadVM",
            let urlParameter = components.queryItems?.first(where: { $0.name == "url" })?.value,
            let url = URL(string: urlParameter) {
-            if alertItem == nil {
-                alertItem = .downloadUrl(url)
+            if data.alertItem == nil {
+                data.showNewVMSheet = false
+                data.alertItem = .downloadUrl(url)
             }
         } else if url.isFileURL {
             data.busyWorkAsync {
@@ -214,19 +205,6 @@ extension ContentView: DropDelegate {
     }
 }
 
-extension ContentView {
-    private enum AlertItem: Identifiable {
-        case downloadUrl(URL)
-
-        var id: Int {
-            switch self {
-            case .downloadUrl(let url):
-                return url.hashValue
-            }
-        }
-    }
-}
-
 struct ContentView_Previews: PreviewProvider {
     static var previews: some View {
         ContentView()

+ 9 - 0
Platform/Shared/VMContextMenuModifier.swift

@@ -183,5 +183,14 @@ struct VMContextMenuModifier: ViewModifier {
                 }
             }
         }
+        #if os(macOS)
+        .onChange(of: (vm.config as? UTMAppleConfiguration)?.isGuestToolsInstallRequested) { newValue in
+            if newValue == true {
+                data.busyWorkAsync {
+                    try await data.mountSupportTools(for: vm.wrapped!)
+                }
+            }
+        }
+        #endif
     }
 }

+ 12 - 1
Platform/Shared/VMWizardState.swift

@@ -59,6 +59,17 @@ enum VMBootDevice: Int, Identifiable {
     case kernel
 }
 
+struct AlertMessage: Identifiable {
+    var message: String
+    public var id: String {
+        message
+    }
+
+    init(_ message: String) {
+        self.message = message
+    }
+}
+
 @MainActor class VMWizardState: ObservableObject {
     let bytesInMib = 1048576
     let bytesInGib = 1073741824
@@ -325,7 +336,6 @@ enum VMBootDevice: Int, Identifiable {
                 bootloader.linuxInitialRamdiskURL = linuxInitialRamdiskURL
                 bootloader.linuxCommandLine = linuxBootArguments
                 config.system.boot = bootloader
-                config.system.genericPlatform = UTMAppleConfigurationGenericPlatform()
                 if let linuxRootImageURL = linuxRootImageURL {
                     config.drives.append(UTMAppleConfigurationDrive(existingURL: linuxRootImageURL))
                     isSkipDiskCreate = true
@@ -333,6 +343,7 @@ enum VMBootDevice: Int, Identifiable {
             } else {
                 config.system.boot = try UTMAppleConfigurationBoot(for: .linux)
             }
+            config.system.genericPlatform = UTMAppleConfigurationGenericPlatform()
             config.virtualization.hasRosetta = linuxHasRosetta
             #endif
         case .Windows:

+ 74 - 19
Platform/UTMData.swift

@@ -36,14 +36,17 @@ typealias ConcreteVirtualMachine = UTMRemoteSpiceVirtualMachine
 typealias ConcreteVirtualMachine = UTMQemuVirtualMachine
 #endif
 
-struct AlertMessage: Identifiable {
-    var message: String
-    public var id: String {
-        message
-    }
-    
-    init(_ message: String) {
-        self.message = message
+enum AlertItem: Identifiable {
+    case message(String)
+    case downloadUrl(URL)
+
+    var id: Int {
+        switch self {
+        case .downloadUrl(let url):
+            return url.hashValue
+        case .message(let message):
+            return message.hashValue
+        }
     }
 }
 
@@ -61,8 +64,8 @@ struct AlertMessage: Identifiable {
     @Published var showNewVMSheet: Bool
     
     /// View: show an alert message
-    @Published var alertMessage: AlertMessage?
-    
+    @Published var alertItem: AlertItem?
+
     /// View: show busy spinner
     @Published var busy: Bool
     
@@ -398,7 +401,7 @@ struct AlertMessage: Identifiable {
     }
     
     func showErrorAlert(message: String) {
-        alertMessage = AlertMessage(message)
+        alertItem = .message(message)
     }
     
     func newVM() {
@@ -470,7 +473,14 @@ struct AlertMessage: Identifiable {
             throw UTMDataError.virtualMachineAlreadyExists
         }
         let vm = try VMData(creatingFromConfig: config, destinationUrl: Self.defaultStorageUrl)
-        try await save(vm: vm)
+        do {
+            try await save(vm: vm)
+        } catch {
+            if isDirectoryEmpty(vm.pathUrl) {
+                try? fileManager.removeItem(at: vm.pathUrl)
+            }
+            throw error
+        }
         listAdd(vm: vm)
         listSelect(vm: vm)
         return vm
@@ -744,7 +754,21 @@ struct AlertMessage: Identifiable {
             }
         }
     }
-    
+
+    private func isDirectoryEmpty(_ pathURL: URL) -> Bool {
+        guard let enumerator = fileManager.enumerator(at: pathURL, includingPropertiesForKeys: [.isDirectoryKey]) else {
+            return false
+        }
+        for case let itemURL as URL in enumerator {
+            let isDirectory = (try? itemURL.resourceValues(forKeys: [.isDirectoryKey]).isDirectory) ?? false
+            if !isDirectory {
+                return false
+            }
+        }
+        // if we get here, we only found empty directories
+        return true
+    }
+
     // MARK: - Downloading VMs
     
     #if os(macOS) && arch(arm64)
@@ -791,11 +815,8 @@ struct AlertMessage: Identifiable {
             listRemove(pendingVM: task.pendingVM)
         }
     }
-    
-    func mountSupportTools(for vm: any UTMVirtualMachine) async throws {
-        guard let vm = vm as? any UTMSpiceVirtualMachine else {
-            throw UTMDataError.unsupportedBackend
-        }
+
+    private func mountWindowsSupportTools(for vm: any UTMSpiceVirtualMachine) async throws {
         let task = UTMDownloadSupportToolsTask(for: vm)
         if await task.hasExistingSupportTools {
             vm.config.qemu.isGuestToolsInstallRequested = false
@@ -813,6 +834,40 @@ struct AlertMessage: Identifiable {
             }
         }
     }
+
+    #if os(macOS)
+    @available(macOS 15, *)
+    private func mountMacSupportTools(for vm: UTMAppleVirtualMachine) async throws {
+        let task = UTMDownloadMacSupportToolsTask(for: vm)
+        if await task.hasExistingSupportTools {
+            vm.config.isGuestToolsInstallRequested = false
+            _ = try await task.mountTools()
+        } else {
+            listAdd(pendingVM: task.pendingVM)
+            Task {
+                do {
+                    _ = try await task.download()
+                } catch {
+                    showErrorAlert(message: error.localizedDescription)
+                }
+                vm.config.isGuestToolsInstallRequested = false
+                listRemove(pendingVM: task.pendingVM)
+            }
+        }
+    }
+    #endif
+
+    func mountSupportTools(for vm: any UTMVirtualMachine) async throws {
+        if let vm = vm as? any UTMSpiceVirtualMachine {
+            return try await mountWindowsSupportTools(for: vm)
+        }
+        #if os(macOS)
+        if #available(macOS 15, *), let vm = vm as? UTMAppleVirtualMachine, vm.config.system.boot.operatingSystem == .macOS {
+            return try await mountMacSupportTools(for: vm)
+        }
+        #endif
+        throw UTMDataError.unsupportedBackend
+    }
     
     /// Cancel a download and discard any data
     /// - Parameter pendingVM: Pending VM to cancel
@@ -961,7 +1016,7 @@ struct AlertMessage: Identifiable {
             } catch {
                 logger.error("\(error)")
                 DispatchQueue.main.async {
-                    self.alertMessage = AlertMessage(error.localizedDescription)
+                    self.alertItem = .message(error.localizedDescription)
                 }
             }
         }

+ 68 - 0
Platform/UTMDownloadMacSupportToolsTask.swift

@@ -0,0 +1,68 @@
+//
+// Copyright © 2022 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
+
+/// Downloads support tools for macOS
+@available(macOS 15, *)
+class UTMDownloadMacSupportToolsTask: UTMDownloadTask {
+    private let vm: UTMAppleVirtualMachine
+
+    private static let supportToolsDownloadUrl = URL(string: "https://getutm.app/downloads/utm-guest-tools-macos-latest.img")!
+
+    private var toolsUrl: URL {
+        fileManager.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!.appendingPathComponent("GuestSupportTools")
+    }
+    
+    private var supportToolsLocalUrl: URL {
+        toolsUrl.appendingPathComponent(Self.supportToolsDownloadUrl.lastPathComponent)
+    }
+
+    @Setting("LastDownloadedMacGuestTools")
+    private var lastDownloadMacGuestTools: Int = 0
+
+    var hasExistingSupportTools: Bool {
+        get async {
+            guard fileManager.fileExists(atPath: supportToolsLocalUrl.path) else {
+                return false
+            }
+            return await lastModifiedTimestamp <= lastDownloadMacGuestTools
+        }
+    }
+    
+    init(for vm: UTMAppleVirtualMachine) {
+        self.vm = vm
+        let name = NSLocalizedString("macOS Guest Support Tools", comment: "UTMDownloadMacSupportToolsTask")
+        super.init(for: Self.supportToolsDownloadUrl, named: name)
+    }
+    
+    override func processCompletedDownload(at location: URL, response: URLResponse?) async throws -> any UTMVirtualMachine {
+        if !fileManager.fileExists(atPath: toolsUrl.path) {
+            try fileManager.createDirectory(at: toolsUrl, withIntermediateDirectories: true)
+        }
+        if fileManager.fileExists(atPath: supportToolsLocalUrl.path) {
+            try fileManager.removeItem(at: supportToolsLocalUrl)
+        }
+        try fileManager.moveItem(at: location, to: supportToolsLocalUrl)
+        lastDownloadMacGuestTools = lastModifiedTimestamp(for: response) ?? 0
+        return try await mountTools()
+    }
+    
+    func mountTools() async throws -> any UTMVirtualMachine {
+        try await vm.attachGuestTools(supportToolsLocalUrl)
+        return vm
+    }
+}

+ 3 - 3
Platform/iOS/Display/zh-HK.lproj/VMDisplayMetalViewInputAccessory.strings

@@ -32,7 +32,7 @@
 "bCv-uH-SSy.normalTitle" = "⌃";
 
 /* Class = "UIButton"; accessibilityLabel = "Num Lock"; ObjectID = "BUk-Vf-yE5"; */
-"BUk-Vf-yE5.accessibilityLabel" = "Num Lock";
+"BUk-Vf-yE5.accessibilityLabel" = "Number Lock";
 
 /* Class = "UIButton"; normalTitle = "Num"; ObjectID = "BUk-Vf-yE5"; */
 "BUk-Vf-yE5.normalTitle" = "Num";
@@ -68,7 +68,7 @@
 "gUX-ez-mbt.normalTitle" = "F3";
 
 /* Class = "UIButton"; accessibilityLabel = "Page Down"; ObjectID = "h4q-XF-UMn"; */
-"h4q-XF-UMn.accessibilityLabel" = "下頁";
+"h4q-XF-UMn.accessibilityLabel" = "下頁";
 
 /* Class = "UIButton"; normalTitle = "Pg Dn"; ObjectID = "h4q-XF-UMn"; */
 "h4q-XF-UMn.normalTitle" = "Pg Dn";
@@ -119,7 +119,7 @@
 "PWe-Va-Qi1.normalTitle" = "F1";
 
 /* Class = "UIButton"; accessibilityLabel = "Page Up"; ObjectID = "pX1-7o-dbU"; */
-"pX1-7o-dbU.accessibilityLabel" = "上頁";
+"pX1-7o-dbU.accessibilityLabel" = "上頁";
 
 /* Class = "UIButton"; normalTitle = "Pg Up"; ObjectID = "pX1-7o-dbU"; */
 "pX1-7o-dbU.normalTitle" = "Pg Up";

+ 3 - 3
Platform/iOS/zh-HK.lproj/Info-RemotePlist.strings

@@ -1,9 +1,9 @@
 /* Bundle name */
-"CFBundleName" = "UTM 遠端";
+"CFBundleName" = "UTM 遙距";
 
 /* Privacy - Local Network Usage Description */
-"NSLocalNetworkUsageDescription" = "UTM 使用本地網絡尋找並連接至 UTM 遠端伺服器。";
+"NSLocalNetworkUsageDescription" = "UTM 使用區域網絡尋找並連接到 UTM 遙距伺服器。";
 
 /* Privacy - Microphone Usage Description */
-"NSMicrophoneUsageDescription" = "任何虛擬電腦都需要許可才能由咪高風進行錄製。";
+"NSMicrophoneUsageDescription" = "任何虛擬機器都需要權限才能從咪高風錄製。";
 

+ 6 - 6
Platform/iOS/zh-HK.lproj/InfoPlist.strings

@@ -2,20 +2,20 @@
 "CFBundleName" = "UTM";
 
 /* Privacy - Local Network Usage Description */
-"NSLocalNetworkUsageDescription" = "虛擬電腦可以訪問本地網絡。UTM 還會使用本地網絡與 AltServer 進行通信。";
+"NSLocalNetworkUsageDescription" = "虛擬機器可以取用區域網絡。UTM 還會使用區域網絡與 AltServer 通訊。";
 
 /* Privacy - Location Always and When In Use Usage Description */
-"NSLocationAlwaysAndWhenInUseUsageDescription" = "UTM 定期請求位置資料,以確保系統保持背景程序處於啟用狀態。位置資料永不離開設備。";
+"NSLocationAlwaysAndWhenInUseUsageDescription" = "UTM 定期請求位置資料,以確保系統保持背景程序處於啟用狀態。位置資料絕對不會離開裝置。";
 
 /* Privacy - Location Always Usage Description */
-"NSLocationAlwaysUsageDescription" = "UTM 定期請求位置資料,以確保系統保持背景程序處於啟用狀態。位置資料永不離開設備。";
+"NSLocationAlwaysUsageDescription" = "UTM 定期請求位置資料,以確保系統保持背景程序處於啟用狀態。位置資料絕對不會離開裝置。";
 
 /* Privacy - Location When In Use Usage Description */
-"NSLocationWhenInUseUsageDescription" = "UTM 定期請求位置資料,以確保系統保持背景程序處於啟用狀態。位置資料永不離開設備。";
+"NSLocationWhenInUseUsageDescription" = "UTM 定期請求位置資料,以確保系統保持背景程序處於啟用狀態。位置資料絕對不會離開裝置。";
 
 /* Privacy - Microphone Usage Description */
-"NSMicrophoneUsageDescription" = "任何虛擬電腦都需要許可才能由咪高風進行錄製。";
+"NSMicrophoneUsageDescription" = "任何虛擬機器都需要權限才能從咪高風錄製。";
 
 /* (No Comment) */
-"UTM virtual machine" = "UTM 虛擬電腦";
+"UTM virtual machine" = "UTM 虛擬機器";
 

+ 1 - 1
Platform/iOS/zh-Hans.lproj/Info-Remote-InfoPlist.strings

@@ -5,5 +5,5 @@
 "NSLocalNetworkUsageDescription" = "UTM 使用本地网络查找和连接 UTM 远程服务器。";
 
 /* Privacy - Microphone Usage Description */
-"NSMicrophoneUsageDescription" = "任何虚拟机都需要获得许可才能从麦克风录音。";
+"NSMicrophoneUsageDescription" = "任何虚拟机都需要权限才能通过麦克风录音。";
 

+ 1 - 1
Platform/iOS/zh-Hans.lproj/Info-RemotePlist.strings

@@ -5,5 +5,5 @@
 "NSLocalNetworkUsageDescription" = "UTM 使用本地网络查找并连接到 UTM 远程服务器。";
 
 /* Privacy - Microphone Usage Description */
-"NSMicrophoneUsageDescription" = "任何虚拟机都需要获得许可才能从麦克风录音。";
+"NSMicrophoneUsageDescription" = "任何虚拟机都需要权限才能通过麦克风录音。";
 

+ 5 - 5
Platform/iOS/zh-Hans.lproj/InfoPlist.strings

@@ -2,19 +2,19 @@
 "CFBundleName" = "UTM";
 
 /* Privacy - Local Network Usage Description */
-"NSLocalNetworkUsageDescription" = "虚拟机可能会访问本地网络。UTM 还使用本地网络与 AltServer 通信。";
+"NSLocalNetworkUsageDescription" = "虚拟机可能会访问本地网络。UTM 还使用本地网络与 AltServer 通信。";
 
 /* Privacy - Location Always and When In Use Usage Description */
-"NSLocationAlwaysAndWhenInUseUsageDescription" = "UTM 定期请求位置数据,以确保系统持后台进程处于活动状态。位置数据永远不会离设备。";
+"NSLocationAlwaysAndWhenInUseUsageDescription" = "UTM 定期请求位置数据,以确保系统持后台进程处于活动状态。位置数据永远不会离设备。";
 
 /* Privacy - Location Always Usage Description */
-"NSLocationAlwaysUsageDescription" = "UTM 定期请求位置数据,以确保系统持后台进程处于活动状态。位置数据永远不会离设备。";
+"NSLocationAlwaysUsageDescription" = "UTM 定期请求位置数据,以确保系统持后台进程处于活动状态。位置数据永远不会离设备。";
 
 /* Privacy - Location When In Use Usage Description */
-"NSLocationWhenInUseUsageDescription" = "UTM 定期请求位置数据,以确保系统持后台进程处于活动状态。位置数据永远不会离设备。";
+"NSLocationWhenInUseUsageDescription" = "UTM 定期请求位置数据,以确保系统持后台进程处于活动状态。位置数据永远不会离设备。";
 
 /* Privacy - Microphone Usage Description */
-"NSMicrophoneUsageDescription" = "任何虚拟机都需要获得许可才能从麦克风录音。";
+"NSMicrophoneUsageDescription" = "任何虚拟机都需要权限才能通过麦克风录音。";
 
 /* (No Comment) */
 "UTM virtual machine" = "UTM 虚拟机";

+ 72 - 4
Platform/macOS/Display/VMDisplayAppleDisplayWindowController.swift

@@ -37,8 +37,12 @@ class VMDisplayAppleDisplayWindowController: VMDisplayAppleWindowController {
         appleConfig.displays.first!.isDynamicResolution
     }
 
+    private let checkSupportsReconfigurationTimeoutPeriod: Double = 1
+    private var checkSupportsReconfigurationTimeoutAttempts: Int = 60
     private var aspectRatioLocked: Bool = false
     private var screenChangedToken: Any?
+    private var isFullscreen: Bool = false
+    private var cancelCheckSupportsReconfiguration: DispatchWorkItem?
 
     @Setting("FullScreenAutoCapture") private var isFullScreenAutoCapture: Bool = false
     
@@ -58,7 +62,11 @@ class VMDisplayAppleDisplayWindowController: VMDisplayAppleWindowController {
     }
 
     override func windowWillClose(_ notification: Notification) {
+        if let screenChangedToken = screenChangedToken {
+            NotificationCenter.default.removeObserver(screenChangedToken)
+        }
         screenChangedToken = nil
+        stopPollingForSupportsReconfiguration()
         super.windowWillClose(notification)
     }
 
@@ -66,6 +74,7 @@ class VMDisplayAppleDisplayWindowController: VMDisplayAppleWindowController {
         appleView.virtualMachine = appleVM.apple
         if #available(macOS 14, *) {
             appleView.automaticallyReconfiguresDisplay = isDynamicResolution
+            startPollingForSupportsReconfiguration()
         }
         super.enterLive()
     }
@@ -77,6 +86,7 @@ class VMDisplayAppleDisplayWindowController: VMDisplayAppleWindowController {
         appleView.virtualMachine = nil
         captureMouseToolbarButton.state = .off
         captureMouseButtonPressed(self)
+        stopPollingForSupportsReconfiguration()
         super.enterSuspended(isBusy: busy)
     }
     
@@ -117,24 +127,31 @@ class VMDisplayAppleDisplayWindowController: VMDisplayAppleWindowController {
     }
     
     func windowDidEnterFullScreen(_ notification: Notification) {
+        isFullscreen = true
         if isFullScreenAutoCapture {
             captureMouseToolbarButton.state = .on
             captureMouseButtonPressed(self)
         }
+        saveDynamicResolution()
     }
     
     func windowDidExitFullScreen(_ notification: Notification) {
+        isFullscreen = false
         if isFullScreenAutoCapture {
             captureMouseToolbarButton.state = .off
             captureMouseButtonPressed(self)
         }
+        saveDynamicResolution()
     }
     
     func windowDidResize(_ notification: Notification) {
-        if aspectRatioLocked && supportsReconfiguration && isDynamicResolution {
-            window!.resizeIncrements = NSSize(width: 1.0, height: 1.0)
-            window!.minSize = NSSize(width: 400, height: 400)
-            aspectRatioLocked = false
+        if supportsReconfiguration && isDynamicResolution {
+            if aspectRatioLocked {
+                window!.resizeIncrements = NSSize(width: 1.0, height: 1.0)
+                window!.minSize = NSSize(width: 400, height: 400)
+                aspectRatioLocked = false
+            }
+            saveDynamicResolution()
         }
     }
 
@@ -150,3 +167,54 @@ class VMDisplayAppleDisplayWindowController: VMDisplayAppleWindowController {
         return CGSize(width: scaledSize.width * scale, height: scaledSize.height * scale)
     }
 }
+
+// MARK: - Save and restore resolution
+@available(macOS 12, *)
+@MainActor extension VMDisplayAppleDisplayWindowController {
+    func saveDynamicResolution() {
+        guard supportsReconfiguration && isDynamicResolution else {
+            return
+        }
+        var resolution = UTMRegistryEntry.Resolution()
+        resolution.isFullscreen = isFullscreen
+        resolution.size = window!.contentRect(forFrameRect: window!.frame).size
+        vm.registryEntry.resolutionSettings[0] = resolution
+    }
+
+    func restoreDynamicResolution(for window: NSWindow) {
+        guard let resolution = vm.registryEntry.resolutionSettings[0] else {
+            return
+        }
+        if resolution.isFullscreen && !isFullscreen {
+            window.toggleFullScreen(self)
+        } else if resolution.size != .zero {
+            let frame = window.frameRect(forContentRect: CGRect(origin: window.frame.origin, size: resolution.size))
+            window.setFrame(frame, display: false, animate: true)
+        }
+    }
+
+    func startPollingForSupportsReconfiguration() {
+        cancelCheckSupportsReconfiguration?.cancel()
+        cancelCheckSupportsReconfiguration = DispatchWorkItem { [weak self] in
+            guard let self = self else {
+                return
+            }
+            if supportsReconfiguration, let window = window {
+                restoreDynamicResolution(for: window)
+                checkSupportsReconfigurationTimeoutAttempts = 0
+                cancelCheckSupportsReconfiguration = nil
+            } else if checkSupportsReconfigurationTimeoutAttempts > 0 {
+                checkSupportsReconfigurationTimeoutAttempts -= 1
+                DispatchQueue.main.asyncAfter(deadline: .now() + checkSupportsReconfigurationTimeoutPeriod, execute: cancelCheckSupportsReconfiguration!)
+            } else {
+                cancelCheckSupportsReconfiguration = nil
+            }
+        }
+        DispatchQueue.main.asyncAfter(deadline: .now() + checkSupportsReconfigurationTimeoutPeriod, execute: cancelCheckSupportsReconfiguration!)
+    }
+
+    func stopPollingForSupportsReconfiguration() {
+        cancelCheckSupportsReconfiguration?.cancel()
+        cancelCheckSupportsReconfiguration = nil
+    }
+}

+ 145 - 1
Platform/macOS/Display/VMDisplayAppleWindowController.swift

@@ -83,13 +83,18 @@ class VMDisplayAppleWindowController: VMDisplayWindowController {
         drivesToolbarItem.isEnabled = false
         usbToolbarItem.isEnabled = false
         resizeConsoleToolbarItem.isEnabled = false
-        if #available(macOS 12, *) {
+        if #available(macOS 13, *) {
+            sharedFolderToolbarItem.isEnabled = true
+        } else if #available(macOS 12, *) {
             sharedFolderToolbarItem.isEnabled = appleConfig.system.boot.operatingSystem == .linux
         } else {
             // stop() not available on macOS 11 for some reason
             restartToolbarItem.isEnabled = false
             sharedFolderToolbarItem.isEnabled = false
         }
+        if #available(macOS 15, *) {
+            drivesToolbarItem.isEnabled = true
+        }
     }
     
     override func enterSuspended(isBusy busy: Bool) {
@@ -116,6 +121,10 @@ class VMDisplayAppleWindowController: VMDisplayWindowController {
         guard #available(macOS 12, *) else {
             return
         }
+        guard appleConfig.system.boot.operatingSystem == .linux else {
+            openShareMenu(sender)
+            return
+        }
         if !isSharePathAlertShownOnce && !isSharePathAlertShownPersistent {
             let alert = NSAlert()
             alert.messageText = NSLocalizedString("Directory sharing", comment: "VMDisplayAppleWindowController")
@@ -256,6 +265,141 @@ extension VMDisplayAppleWindowController {
     }
 }
 
+@objc extension VMDisplayAppleWindowController {
+    @IBAction override func drivesButtonPressed(_ sender: Any) {
+        let menu = NSMenu()
+        menu.autoenablesItems = false
+        let item = NSMenuItem()
+        item.title = NSLocalizedString("Querying drives status...", comment: "VMDisplayWindowController")
+        item.isEnabled = false
+        menu.addItem(item)
+        updateDrivesMenu(menu, drives: appleConfig.drives)
+        menu.popUp(positioning: nil, at: NSEvent.mouseLocation, in: nil)
+    }
+
+    @nonobjc func updateDrivesMenu(_ menu: NSMenu, drives: [UTMAppleConfigurationDrive]) {
+        menu.removeAllItems()
+        if drives.count == 0 {
+            let item = NSMenuItem()
+            item.title = NSLocalizedString("No drives connected.", comment: "VMDisplayWindowController")
+            item.isEnabled = false
+            menu.addItem(item)
+        }
+        if #available(macOS 15, *), appleConfig.system.boot.operatingSystem == .macOS {
+            let item = NSMenuItem()
+            item.title = NSLocalizedString("Install Guest Tools…", comment: "VMDisplayAppleWindowController")
+            item.isEnabled = !appleConfig.isGuestToolsInstallRequested
+            item.state = appleVM.hasGuestToolsAttached ? .on : .off
+            item.target = self
+            item.action = #selector(installGuestTools)
+            menu.addItem(item)
+        }
+        for i in drives.indices {
+            let drive = drives[i]
+            if !drive.isExternal {
+                continue // skip non-disks
+            }
+            let item = NSMenuItem()
+            item.title = label(for: drive)
+            if !drive.isExternal {
+                item.isEnabled = false
+            } else if #available(macOS 15, *) {
+                let submenu = NSMenu()
+                submenu.autoenablesItems = false
+                let eject = NSMenuItem(title: NSLocalizedString("Eject", comment: "VMDisplayWindowController"),
+                                       action: #selector(ejectDrive),
+                                       keyEquivalent: "")
+                eject.target = self
+                eject.tag = i
+                eject.isEnabled = drive.imageURL != nil
+                submenu.addItem(eject)
+                let change = NSMenuItem(title: NSLocalizedString("Change", comment: "VMDisplayWindowController"),
+                                        action: #selector(changeDriveImage),
+                                        keyEquivalent: "")
+                change.target = self
+                change.tag = i
+                change.isEnabled = true
+                submenu.addItem(change)
+                item.submenu = submenu
+            }
+            menu.addItem(item)
+        }
+        menu.update()
+    }
+
+    @nonobjc private func withErrorAlert(_ callback: @escaping () async throws -> Void) {
+        Task.detached(priority: .background) { [self] in
+            do {
+                try await callback()
+            } catch {
+                Task { @MainActor in
+                    showErrorAlert(error.localizedDescription)
+                }
+            }
+        }
+    }
+
+    @available(macOS 15, *)
+    func ejectDrive(sender: AnyObject) {
+        guard let menu = sender as? NSMenuItem else {
+            logger.error("wrong sender for ejectDrive")
+            return
+        }
+        let drive = appleConfig.drives[menu.tag]
+        withErrorAlert {
+            try await self.appleVM.eject(drive)
+        }
+    }
+
+    @available(macOS 15, *)
+    func openDriveImage(forDriveIndex index: Int) {
+        let drive = appleConfig.drives[index]
+        let openPanel = NSOpenPanel()
+        openPanel.title = NSLocalizedString("Select Drive Image", comment: "VMDisplayWindowController")
+        openPanel.allowedContentTypes = [.data]
+        openPanel.beginSheetModal(for: window!) { response in
+            guard response == .OK else {
+                return
+            }
+            guard let url = openPanel.url else {
+                logger.debug("no file selected")
+                return
+            }
+            self.withErrorAlert {
+                try await self.appleVM.changeMedium(drive, to: url)
+            }
+        }
+    }
+
+    @available(macOS 15, *)
+    func changeDriveImage(sender: AnyObject) {
+        guard let menu = sender as? NSMenuItem else {
+            logger.error("wrong sender for ejectDrive")
+            return
+        }
+        openDriveImage(forDriveIndex: menu.tag)
+    }
+
+    @nonobjc private func label(for drive: UTMAppleConfigurationDrive) -> String {
+        let imageURL = drive.imageURL
+        return String.localizedStringWithFormat(NSLocalizedString("USB Mass Storage: %@", comment: "VMDisplayAppleDisplayController"),
+                                                imageURL?.lastPathComponent ?? NSLocalizedString("none", comment: "VMDisplayAppleDisplayController"))
+    }
+
+    @available(macOS 15, *)
+    @MainActor private func installGuestTools(sender: AnyObject) {
+        if appleVM.hasGuestToolsAttached {
+            withErrorAlert {
+                try await self.appleVM.detachGuestTools()
+            }
+        } else {
+            showConfirmAlert(NSLocalizedString("An USB device containing the installer will be mounted in the virtual machine. Only macOS Sequoia (15.0) and newer guests are supported.", comment: "VMDisplayAppleDisplayController")) {
+                self.appleConfig.isGuestToolsInstallRequested = true
+            }
+        }
+    }
+}
+
 extension VMDisplayAppleWindowController: UTMScreenshotProvider {
     var screenshot: UTMVirtualMachineScreenshot? {
         if let image = mainView?.image() {

+ 41 - 3
Platform/macOS/Display/VMDisplayQemuMetalWindowController.swift

@@ -113,12 +113,19 @@ class VMDisplayQemuMetalWindowController: VMDisplayQemuWindowController {
             }
         }
 
+        if isSecondary && isDisplaySizeDynamic, let window = window {
+            restoreDynamicResolution(for: window)
+        }
+
         super.windowDidLoad()
     }
     
     override func windowWillClose(_ notification: Notification) {
         vmDisplay?.removeRenderer(renderer!)
         stopAllCapture()
+        if let screenChangedToken = screenChangedToken {
+            NotificationCenter.default.removeObserver(screenChangedToken)
+        }
         screenChangedToken = nil
         super.windowWillClose(notification)
     }
@@ -245,10 +252,10 @@ extension VMDisplayQemuMetalWindowController {
             return
         }
         if isDisplaySizeDynamic != supported {
-            displaySizeDidChange(size: displaySize)
+            displaySizeDidChange(size: displaySize, shouldSaveResolution: false)
             DispatchQueue.main.async {
                 if supported, let window = self.window {
-                    _ = self.updateGuestResolution(for: window, frameSize: window.frame.size)
+                    self.restoreDynamicResolution(for: window)
                 }
             }
         }
@@ -259,7 +266,7 @@ extension VMDisplayQemuMetalWindowController {
     
 // MARK: - Screen management
 extension VMDisplayQemuMetalWindowController {
-    fileprivate func displaySizeDidChange(size: CGSize) {
+    fileprivate func displaySizeDidChange(size: CGSize, shouldSaveResolution: Bool = true) {
         // cancel any pending resize
         cancelResize?.cancel()
         cancelResize = nil
@@ -279,6 +286,9 @@ extension VMDisplayQemuMetalWindowController {
             } else {
                 self.updateHostFrame(forGuestResolution: size)
             }
+            if shouldSaveResolution {
+                self.saveDynamicResolution()
+            }
         }
     }
     
@@ -409,6 +419,7 @@ extension VMDisplayQemuMetalWindowController {
         if isFullScreenAutoCapture {
             captureMouse()
         }
+        saveDynamicResolution()
     }
     
     func windowDidExitFullScreen(_ notification: Notification) {
@@ -416,6 +427,7 @@ extension VMDisplayQemuMetalWindowController {
         if isFullScreenAutoCapture {
             releaseMouse()
         }
+        saveDynamicResolution()
     }
     
     func windowDidBecomeMain(_ notification: Notification) {
@@ -446,6 +458,32 @@ extension VMDisplayQemuMetalWindowController {
     }
 }
 
+// MARK: - Save and restore resolution
+@MainActor extension VMDisplayQemuMetalWindowController {
+    func saveDynamicResolution() {
+        guard isDisplaySizeDynamic else {
+            return
+        }
+        var resolution = UTMRegistryEntry.Resolution()
+        resolution.isFullscreen = isFullScreen
+        resolution.size = displaySize
+        vm.registryEntry.resolutionSettings[id] = resolution
+    }
+
+    func restoreDynamicResolution(for window: NSWindow) {
+        guard let resolution = vm.registryEntry.resolutionSettings[id] else {
+            return
+        }
+        if resolution.isFullscreen && !isFullScreen {
+            window.toggleFullScreen(self)
+        } else if resolution.size != .zero {
+            _ = self.updateGuestResolution(for: window, frameSize: resolution.size)
+        } else {
+            _ = self.updateGuestResolution(for: window, frameSize: window.frame.size)
+        }
+    }
+}
+
 // MARK: - Input events
 extension VMDisplayQemuMetalWindowController: VMMetalViewInputDelegate {
     var shouldUseCmdOptForCapture: Bool {

+ 1 - 1
Platform/macOS/Display/zh-Hans.lproj/VMDisplayWindow.strings

@@ -86,7 +86,7 @@
 "Ulf-oT-4cP.paletteLabel" = "重新调整控制台";
 
 /* Class = "NSToolbarItem"; toolTip = "Send console resize command"; ObjectID = "Ulf-oT-4cP"; */
-"Ulf-oT-4cP.toolTip" = "发送控制台重新调整命令";
+"Ulf-oT-4cP.toolTip" = "发送控制台重新调整大小命令";
 
 /* Class = "NSButton"; ibShadowedToolTip = "Starts/resumes the VM"; ObjectID = "ZTi-Hs-ge6"; */
 "ZTi-Hs-ge6.ibShadowedToolTip" = "启动/恢复虚拟机";

+ 1 - 1
Platform/macOS/UTMDataExtension.swift

@@ -50,7 +50,7 @@ extension UTMData {
             }
             if window == nil {
                 DispatchQueue.main.async {
-                    self.alertMessage = AlertMessage(NSLocalizedString("This virtual machine cannot be run on this machine.", comment: "UTMDataExtension"))
+                    self.alertItem = .message(NSLocalizedString("This virtual machine cannot be run on this machine.", comment: "UTMDataExtension"))
                 }
             }
         }

+ 23 - 5
Platform/macOS/VMAppleRemovableDrivesView.swift

@@ -46,7 +46,15 @@ struct VMAppleRemovableDrivesView: View {
             return false
         }
     }
-    
+
+    private var hasLiveRemovableDrives: Bool {
+        if #available(macOS 15, *) {
+            return true
+        } else {
+            return false
+        }
+    }
+
     var body: some View {
         Group {
             ForEach($registryEntry.sharedDirectories) { $sharedDirectory in
@@ -95,7 +103,7 @@ struct VMAppleRemovableDrivesView: View {
                             }
                         } label: {
                             Label("External Drive", systemImage: "externaldrive")
-                        }.disabled(vm.hasSuspendState || vm.state != .stopped)
+                        }.disabled(vm.hasSuspendState || (vm.state != .stopped && !hasLiveRemovableDrives))
                     } else {
                         Label("\(diskImage.sizeString) Drive", systemImage: "internaldrive")
                     }
@@ -196,13 +204,23 @@ struct VMAppleRemovableDrivesView: View {
     private func selectRemovableImage(for diskImage: UTMAppleConfigurationDrive, result: Result<URL, Error>) {
         data.busyWorkAsync {
             let url = try result.get()
-            let file = try UTMRegistryEntry.File(url: url)
-            await registryEntry.setExternalDrive(file, forId: diskImage.id)
+            if #available(macOS 15, *) {
+                try await appleVM.changeMedium(diskImage, to: url)
+            } else {
+                let file = try UTMRegistryEntry.File(url: url)
+                await registryEntry.setExternalDrive(file, forId: diskImage.id)
+            }
         }
     }
     
     private func clearRemovableImage(_ diskImage: UTMAppleConfigurationDrive) {
-        registryEntry.removeExternalDrive(forId: diskImage.id)
+        data.busyWorkAsync {
+            if #available(macOS 15, *) {
+                try await appleVM.eject(diskImage)
+            } else {
+                await registryEntry.removeExternalDrive(forId: diskImage.id)
+            }
+        }
     }
 }
 

+ 2 - 1
Platform/macOS/VMConfigAppleVirtualizationView.swift

@@ -36,7 +36,8 @@ struct VMConfigAppleVirtualizationView: View {
                 Toggle("Enable Rosetta on Linux (x86_64 Emulation)", isOn: $config.hasRosetta.bound)
                     .help("If enabled, a virtiofs share tagged 'rosetta' will be available on the Linux guest for installing Rosetta for emulating x86_64 on ARM64.")
                 #endif
-                
+            }
+            if #available(macOS 13, *) {
                 Toggle("Enable Clipboard Sharing", isOn: $config.hasClipboardSharing)
                     .help("Requires SPICE guest agent tools to be installed.")
             }

+ 2 - 2
Platform/macOS/zh-HK.lproj/InfoPlist.strings

@@ -2,8 +2,8 @@
 "CFBundleName" = "UTM";
 
 /* Privacy - Microphone Usage Description */
-"NSMicrophoneUsageDescription" = "任何虛擬電腦都需要許可才能由咪高風進行錄製。";
+"NSMicrophoneUsageDescription" = "任何虛擬機器都需要權限才能從咪高風錄製。";
 
 /* (No Comment) */
-"UTM virtual machine" = "UTM 虛擬電腦";
+"UTM virtual machine" = "UTM 虛擬機器";
 

+ 1 - 1
Platform/macOS/zh-Hans.lproj/InfoPlist.strings

@@ -2,7 +2,7 @@
 "CFBundleName" = "UTM";
 
 /* Privacy - Microphone Usage Description */
-"NSMicrophoneUsageDescription" = "任何虚拟机都需要获得权限才能从麦克风进行录音。";
+"NSMicrophoneUsageDescription" = "任何虚拟机都需要权限才能通过麦克风录音。";
 
 /* (No Comment) */
 "UTM virtual machine" = "UTM 虚拟机";

File diff suppressed because it is too large
+ 163 - 163
Platform/zh-HK.lproj/Localizable.strings


+ 96 - 96
Platform/zh-Hans.lproj/Localizable.strings

@@ -81,16 +81,16 @@
 "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." = "发生了内部错误。";
 
 /* UTMConfiguration */
-"An invalid value of '%@' is used in the configuration file." = "配置文件中使用了无效值 '%@'。";
+"An invalid value of '%@' is used in the configuration file." = "配置文件中使用了无效值“%@”。";
 
 /* UTMRemoteSpiceVirtualMachine */
-"An operation is already in progress." = "一项操作已在进行中。";
+"An operation is already in progress." = "一项操作已经进行。";
 
 /* UTMQemuImage */
 "An unknown QEMU error has occurred." = "发生了未知的 QEMU 错误。";
@@ -102,7 +102,7 @@
 "ANGLE (OpenGL)" = "ANGLE (OpenGL)";
 
 /* VMConfigSystemView */
-"Any unsaved changes will be lost." = "任何未保存的更改都将丢失。";
+"Any unsaved changes will be lost." = "所有未存储的更改都将丢失。";
 
 /* No comment provided by engineer. */
 "Approve" = "批准";
@@ -111,16 +111,16 @@
 "Architecture" = "架构";
 
 /* No comment provided by engineer. */
-"Are you sure you want to exit UTM?" = "确定要退出 UTM 吗?";
+"Are you sure you want to exit UTM?" = "确定要退出 UTM 吗?";
 
 /* No comment provided by engineer. */
-"Are you sure you want to permanently delete this disk image?" = "确定要永久删除此磁盘映像吗?";
+"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. */
 "Authentication" = "认证";
@@ -209,22 +209,22 @@
 "Cannot find VM with ID: %@" = "无法通过 ID 找到虚拟机:%@";
 
 /* 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 不兼容的平台上创建。";
 
 /* UTMRemoteServer */
-"Cannot reserve port %d for external access from NAT. Make sure no other device on the network has reserved it." = "无法保留端口“%d”用作从 NAT 的外部访问。请确保网络上没有其他设备保留该端口。";
+"Cannot reserve port %d for external access from NAT. Make sure no other device on the network has reserved it." = "无法保留端口 %d 用作通过 NAT 的外部访问。确保网络上没有其他设备保留该端口。";
 
 /* No comment provided by engineer. */
-"Caps Lock (⇪) is treated as a key" = "将 Caps Lock (⇪) 视为按键";
+"Caps Lock (⇪) is treated as a key" = "将 Caps Lock (⇪) 视为按键处理";
 
 /* VMMetalView */
 "Capture Input" = "捕获输入";
 
 /* No comment provided by engineer. */
-"Capture input automatically when entering full screen" = "进入全屏时自动捕获输入";
+"Capture input automatically when entering full screen" = "进入全屏时自动捕获输入";
 
 /* No comment provided by engineer. */
-"Capture input automatically when window is focused" = "聚焦窗口时自动捕获输入";
+"Capture input automatically when window is focused" = "窗口聚焦时自动捕获输入";
 
 /* VMDisplayQemuMetalWindowController */
 "Captured mouse" = "已捕获鼠标";
@@ -255,7 +255,7 @@
 "Close" = "关闭";
 
 /* VMQemuDisplayMetalWindowController */
-"Closing this window will kill the VM." = "关闭此窗口将终止虚拟机。";
+"Closing this window will kill the VM." = "关闭此窗口将终止虚拟机。";
 
 /* VMQemuDisplayMetalWindowController */
 "Confirm" = "确认";
@@ -277,7 +277,7 @@
 "Connection" = "连接";
 
 /* VMSessionState */
-"Connection to the server was lost." = "与服务器的连接丢失。";
+"Connection to the server was lost." = "与服务器的连接丢失。";
 
 /* No comment provided by engineer. */
 "Console" = "控制台";
@@ -286,7 +286,7 @@
 "Continue" = "继续";
 
 /* No comment provided by engineer. */
-"CoreAudio (Output Only)" = "CoreAudio (仅输出)";
+"CoreAudio (Output Only)" = "CoreAudio (仅输出)";
 
 /* No comment provided by engineer. */
 "Cores" = "核心";
@@ -307,7 +307,7 @@
 "Create a New Virtual Machine" = "创建一个新虚拟机";
 
 /* No comment provided by engineer. */
-"Create a new virtual machine or import an existing one." = "创建一个新虚拟机或导入现有的虚拟机。";
+"Create a new virtual machine or import an existing one." = "创建一个新虚拟机或导入现有的虚拟机。";
 
 /* VMConfigAppleDisplayView */
 "Custom" = "自定义";
@@ -338,7 +338,7 @@
 "Directory sharing" = "目录共享";
 
 /* UTMQemuConstants */
-"Disabled" = "已用";
+"Disabled" = "已用";
 
 /* No comment provided by engineer. */
 "Disconnect" = "断开连接";
@@ -360,7 +360,7 @@
 "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" = "关闭正在运行的虚拟机时不显示确认";
@@ -369,31 +369,31 @@
 "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?" = "要删除此虚拟机及其所有数据吗?";
+"Do you want to delete this VM and all its data?" = "要删除此虚拟机及其所有数据吗?";
 
 /* No comment provided by engineer. */
-"Do you want to download '%@'?" = "要下载 '%@' 吗?";
+"Do you want to download '%@'?" = "你要下载“%@”吗?";
 
 /* No comment provided by engineer. */
-"Do you want to duplicate this VM and all its data?" = "要复制此虚拟机及其所有数据吗?";
+"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 forget all clients and generate a new server identity? Any clients that previously paired with this server will be instructed to manually unpair with this server before they can connect again." = "要忘记所有客户端并生成新的服务器身份吗?之前与此服务器配对的任何客户端将被告知手动取消与此服务器的配对,之后才能再次连接。";
+"Do you want to forget all clients and generate a new server identity? Any clients that previously paired with this server will be instructed to manually unpair with this server before they can connect again." = "你要忽略所有客户端并生成新的服务器身份吗?之前与此服务器配对的任何客户端将被告知手动取消与此服务器的配对,之后才能再次连接。";
 
 /* No comment provided by engineer. */
-"Do you want to forget the selected client(s)?" = "要忘记所选的客户端吗?";
+"Do you want to forget the selected client(s)?" = "要忘记所选的客户端吗?";
 
 /* 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" = "下载";
@@ -456,10 +456,10 @@
 "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。";
+"Failed to attach to JitStreamer." = "无法附加到 JitStreamer。";
 
 /* UTMSpiceIO */
 "Failed to change current directory." = "更改当前目录失败。";
@@ -474,7 +474,7 @@
 "Failed to create pipe for communications." = "无法为通信创建管道。";
 
 /* UTMData */
-"Failed to decode JitStreamer response." = "未能解码 JitStreamer 响应。";
+"Failed to decode JitStreamer response." = "无法解码 JitStreamer 响应。";
 
 /* UTMRemoteClient */
 "Failed to determine host name." = "无法确定主机名。";
@@ -511,10 +511,10 @@
 
 /* AppDelegate
    VMDisplayWindowController */
-"Failed to save suspend state" = "无法存挂起状态。";
+"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 客户端。";
@@ -539,7 +539,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" = "强制多核";
@@ -668,7 +668,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" = "许可";
@@ -840,7 +840,7 @@
 "OK" = "好";
 
 /* UTMScriptingVirtualMachineImpl */
-"One or more required parameters are missing or invalid." = "一个或多个必参数缺失或无效。";
+"One or more required parameters are missing or invalid." = "一个或多个必填的参数缺失或无效。";
 
 /* No comment provided by engineer. */
 "Open…" = "打开…";
@@ -855,7 +855,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. */
 "Options" = "选项";
@@ -885,7 +885,7 @@
 "PC System Flash" = "PC 系统闪存";
 
 /* No comment provided by engineer. */
-"Pending" = "等待中";
+"Pending" = "待处理";
 
 /* UTMDonateView */
 "period" = "周期";
@@ -894,7 +894,7 @@
 "Play" = "启动";
 
 /* VMWizardState */
-"Please select a boot image." = "请选择一个引导映像。";
+"Please select a boot image." = "请选择一个启动映像。";
 
 /* VMWizardState */
 "Please select a kernel file." = "请选择一个内核文件。";
@@ -921,10 +921,10 @@
 "Press %@ to release cursor" = "按下 %@ 释放光标";
 
 /* No comment provided by engineer. */
-"Prevent system from sleeping when any VM is running" = "当任何虚拟机运行时防止系统处于眠状态";
+"Prevent system from sleeping when any VM is running" = "当任何虚拟机运行时防止系统处于眠状态";
 
 /* UTMQemuConstants */
-"Pseudo-TTY Device" = "虚拟终端设备";
+"Pseudo-TTY Device" = "虚拟终端设备";
 
 /* No comment provided by engineer. */
 "QEMU Arguments" = "QEMU 参数";
@@ -999,7 +999,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" = "正在恢复";
@@ -1017,16 +1017,16 @@
 "Running" = "正在运行";
 
 /* No comment provided by engineer. */
-"Running low on memory! UTM might soon be killed by iOS. You can prevent this by decreasing the amount of memory and/or JIT cache assigned to this VM" = "运行内存不足!UTM 可能很快就会被 iOS 终止。你可以通过减少分配给此虚拟机的内存和 (或) JIT 缓存来防止这种情况。";
+"Running low on memory! UTM might soon be killed by iOS. You can prevent this by decreasing the amount of memory and/or JIT cache assigned to this VM" = "运行内存不足!UTM 可能很快就会被 iOS 终止。你可以通过减少分配给此虚拟机的内存和/或 JIT 缓存来防止这种情况。";
 
 /* No comment provided by engineer. */
-"Save" = "存";
+"Save" = "存";
 
 /* No comment provided by engineer. */
-"Saved" = "已存";
+"Saved" = "已存";
 
 /* VMData */
-"Saving" = "正在存";
+"Saving" = "正在存";
 
 /* No comment provided by engineer. */
 "Scaling" = "粗化";
@@ -1054,10 +1054,10 @@
 "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:" = "已选择:";
@@ -1115,7 +1115,7 @@
 "Socket not specified." = "未指定套接字。";
 
 /* 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";
@@ -1166,7 +1166,7 @@
 "Suspend is not supported when GPU acceleration is enabled." = "启用 GPU 加速时不支持挂起。";
 
 /* UTMQemuVirtualMachine */
-"Suspend state cannot be saved when running in disposible mode." = "在一次性模式下运行时无法存挂起状态。";
+"Suspend state cannot be saved when running in disposible mode." = "在一次性模式下运行时无法存挂起状态。";
 
 /* VMData */
 "Suspended" = "已挂起";
@@ -1193,7 +1193,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" = "测试";
@@ -1217,16 +1217,16 @@
 "The device is not currently connected." = "设备目前尚未连接。";
 
 /* UTMConfiguration */
-"The drive '%@' already exists and cannot be created." = "驱动器 '%@' 已存在,无法创建。";
+"The drive '%@' already exists and cannot be created." = "驱动器“%@”已存在,无法创建。";
 
 /* UTMDownloadSupportToolsTaskError */
 "The guest support tools have already been mounted." = "客户机支持工具已挂载。";
 
 /* UTMRemoteClient */
-"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." = "主机指纹与存的值不匹配。这意味着 UTM 服务器被重置、不同的主机使用相同的名称,或者攻击者正在冒充主机。为了保护你的安全,你需要删除已存的主机才能继续。";
+"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." = "主机指纹与存的值不匹配。这意味着 UTM 服务器被重置、不同的主机使用相同的名称,或者攻击者正在冒充主机。为了保护你的安全,你需要删除已存的主机才能继续。";
 
 /* UTMAppleConfiguration */
-"The host operating system needs to be updated to support one or more features requested by the guest." = "需要更新主机操作系统以支持客户机请求的一个或多个功能。";
+"The host operating system needs to be updated to support one or more features requested by the guest." = "需要更新主机操作系统以支持客户机请求的一个或多个功能。";
 
 /* UTMAppleVirtualMachine */
 "The operating system cannot be installed on this machine." = "操作系统无法安装在此机器上。";
@@ -1241,7 +1241,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 '%@'." = "所选的启动映像名称包含“%@”,但客户机的架构为“%@”。请确保你选择了与“%@”兼容的映像。";
 
 /* UTMRemoteClient */
 "The server interface version does not match the client." = "服务器接口版本与客户端不匹配。";
@@ -1277,7 +1277,7 @@
 "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." = "此配置过旧,无法支持。";
@@ -1301,7 +1301,7 @@
 "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." = "你的机器不支持此操作系统。";
@@ -1319,10 +1319,10 @@
 "This virtual machine has been removed." = "此虚拟机已被移除。";
 
 /* UTMDataExtension */
-"This virtual machine is already running. In order to run it from this device, you must stop it first." = "此虚拟机已在运行。要从该设备运行此虚拟机,你必须先停止它。";
+"This virtual machine is already running. In order to run it from this device, you must stop it first." = "此虚拟机已在运行。要从该设备运行此虚拟机,你必须先停止它。";
 
 /* UTMData */
-"This virtual machine is currently unavailable, make sure it is not open in another session." = "此虚拟机当前不可用,确保它没有在另一个会话中打开。";
+"This virtual machine is currently unavailable, make sure it is not open in another session." = "此虚拟机当前不可用,确保它没有在另一个会话中打开。";
 
 /* VMData */
 "This VM is not available or is configured for a backend that does not support remote clients." = "此虚拟机不可用,或配置为不支持远程客户端的后端。";
@@ -1331,7 +1331,7 @@
 "This VM is unavailable." = "此虚拟机不可用。";
 
 /* 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." = "这将重置虚拟机,任何未存的状态都将丢失。";
 
 /* UTMRemoteConnectView */
 "Timed out trying to connect." = "尝试连接超时。";
@@ -1343,10 +1343,10 @@
 "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。";
+"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。";
 
 /* VMDisplayQemuMetalWindowController */
-"To release the mouse cursor, press %@ at the same time." = "要释放鼠标光标,请同时按 %@。";
+"To release the mouse cursor, press %@ at the same time." = "要释放鼠标光标,请同时按 %@。";
 
 /* No comment provided by engineer. */
 "Trust" = "信任";
@@ -1456,16 +1456,16 @@
 "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 吗?若现有的操作系统已安装在该虚拟机的主驱动器上,则它将被抹掉。";
+"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." = "要重新转换此磁盘映像以回收未使用的空间吗?请注意,这将需要足够的临时空间来执行转换。在继续操作之前,强烈建议你备份此虚拟机。";
 
 /* UTMDonateView */
 "year" = "年";
@@ -1484,7 +1484,7 @@
 "Your purchase could not be verified by the App Store." = "App Store 无法验证你的购买。";
 
 /* No comment provided by engineer. */
-"Your support is the driving force that helps UTM stay independent. Your contribution, no matter the size, makes a significant difference. It enables us to develop new features and maintain existing ones. Thank you for considering a donation to support us." = "你的支持是 UTM 保持独立的动力。无论你的贡献少,都会产生重大的影响。这可以让我们开发功能,并维护现有的功能。感谢你考虑捐赠支持我们。";
+"Your support is the driving force that helps UTM stay independent. Your contribution, no matter the size, makes a significant difference. It enables us to develop new features and maintain existing ones. Thank you for considering a donation to support us." = "你的支持是 UTM 保持独立的动力。无论你的贡献多少,都会产生重大的影响。这可以让我们开发功能,并维护现有的功能。感谢你考虑捐赠支持我们。";
 
 /* 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/。";
@@ -1513,7 +1513,7 @@
 "Advanced" = "高级";
 
 /* 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 磁盘映像不支持快照,也不会动态地扩充大小。";
+"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 access from external clients" = "允许外部客户机访问";
@@ -1588,10 +1588,10 @@
 "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." = "默认情況下,将使用目标虚拟机的最佳后端。若所选的后端任何原因而不可用,将自动选择替代方案。";
+"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 虚拟机。";
+"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..." = "计算当前大小…";
@@ -1612,7 +1612,7 @@
 "Clone" = "复制";
 
 /* No comment provided by engineer. */
-"Clone selected VM" = "复制已选的虚拟机";
+"Clone selected VM" = "复制已选的虚拟机";
 
 /* No comment provided by engineer. */
 "Clone…" = "复制…";
@@ -1648,10 +1648,10 @@
 "Delete this drive." = "删除此驱动器。";
 
 /* No comment provided by engineer. */
-"Delete selected VM" = "刪除已选的虚拟机";
+"Delete selected VM" = "刪除已选的虚拟机";
 
 /* No comment provided by engineer. */
-"Delete this shortcut. The underlying data will not be deleted." = "删除此快捷方式。快捷方式背后的数据不会被删除。";
+"Delete this shortcut. The underlying data will not be deleted." = "删除此快捷方式。快捷方式背后指向的数据不会被删除。";
 
 /* No comment provided by engineer. */
 "Delete this VM and all its data." = "刪除此虚拟机及其所有数据。";
@@ -1699,7 +1699,7 @@
 "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 的客户机支持包。此支持包对于一些功能而言为必需,例如动态分辨率与剪贴板共享。";
+"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 客户机工具。";
@@ -1717,7 +1717,7 @@
 "Edit" = "编辑";
 
 /* No comment provided by engineer. */
-"Edit selected VM" = "编辑已选的虚拟机";
+"Edit selected VM" = "编辑已选的虚拟机";
 
 /* No comment provided by engineer. */
 "Edit…" = "编辑…";
@@ -1735,7 +1735,7 @@
 "Emulated Network Card" = "模拟网卡";
 
 /* No comment provided by engineer. */
-"Emulated Serial Device" = "模拟序列设备";
+"Emulated Serial Device" = "模拟串行设备";
 
 /* No comment provided by engineer. */
 "Enable Balloon Device" = "启用 Balloon 设备";
@@ -1786,7 +1786,7 @@
 "Font" = "字体";
 
 /* No comment provided by engineer. */
-"Force Disable CPU Flags" = "强制用 CPU 标志";
+"Force Disable CPU Flags" = "强制用 CPU 标志";
 
 /* No comment provided by engineer. */
 "Force Enable CPU Flags" = "强制启用 CPU 标志";
@@ -1849,7 +1849,7 @@
 "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 标志。否则将使用默认值。";
+"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." = "若选中,驱动器映像将和虚拟机一起存储。";
@@ -1859,10 +1859,10 @@
 
 /* VMConfigAppleDriveDetailsView
  VMConfigAppleDriveCreateView*/
-"If checked, use NVMe instead of virtio as the disk interface, available on macOS 14+ for Linux guests only. This interface is slower but less likely to encounter filesystem errors." = "若选中,使用 NVMe 而不是 virtio 作为磁盘接口,仅适用于 macOS 14+ 上的 Linux 客户机。此接口速度较慢,但不太容易遇到文件系统错误。";
+"If checked, use NVMe instead of virtio as the disk interface, available on macOS 14+ for Linux guests only. This interface is slower but less likely to encounter filesystem errors." = "若选中,使用 NVMe 而不是 virtio 作为磁盘接口,仅适用于 macOS 14+ 上的 Linux 客户机。此接口速度较慢,但不太容易遇到文件系统错误。";
 
 /* No comment provided by engineer. */
-"If disabled, the default combination Control+Option (⌃+⌥) will be used." = "若用,将使用默认组合键 Control + Option (⌃ + ⌥)。";
+"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。";
@@ -1871,7 +1871,7 @@
 "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 将和其他按键一样处理。若用,它将被视为与主机同步的切换键。";
+"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." = "若启用,输入捕捉会在进入和退出全屏模式时自动切换。";
@@ -1880,7 +1880,7 @@
 "If enabled, input capture will toggle automatically when the VM's window is focused." = "若启用,输入捕捉将在虚拟机窗口聚焦时自动切换。";
 
 /* 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 指示灯不同步。";
+"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 键将按照系统默认方式工作 (例如输入国际文本)。";
@@ -1898,7 +1898,7 @@
 "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 启动。";
+"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" = "映像类型";
@@ -1952,7 +1952,7 @@
 "Last Seen" = "最后上线于";
 
 /* No comment provided by engineer. */
-"Legacy Hardware" = "旧硬件";
+"Legacy Hardware" = "旧硬件";
 
 /* No comment provided by engineer. */
 "MAC Address" = "MAC 地址";
@@ -1979,7 +1979,7 @@
 "Move…" = "移动…";
 
 /* No comment provided by engineer. */
-"Move selected VM" = "移动已选的虚拟机";
+"Move selected VM" = "移动已选的虚拟机";
 
 /* No comment provided by engineer. */
 "Move this VM from internal storage to elsewhere." = "将此虚拟机从内部存储空间移动到其他地方。";
@@ -2024,7 +2024,7 @@
 "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." = "此处的选项只在下次启动时生效,不会存。";
+"Options here only apply on next boot and are not saved." = "此处的选项只在下次启动时生效,不会存。";
 
 /* No comment provided by engineer. */
 "Path" = "路径";
@@ -2072,7 +2072,7 @@
 "Reject unknown connections by default" = "默认情况下拒绝未知连接";
 
 /* No comment provided by engineer. */
-"Remove selected shortcut" = "移除选的快捷方式";
+"Remove selected shortcut" = "移除选的快捷方式";
 
 /* No comment provided by engineer. */
 "Renderer Backend" = "渲染器后端";
@@ -2087,7 +2087,7 @@
 "Requires SPICE guest agent tools to be installed." = "需要安装 SPICE 客户机代理工具。";
 
 /* No comment provided by engineer. */
-"Reset UEFI Variables" = "重 UEFI 变量";
+"Reset UEFI Variables" = "重 UEFI 变量";
 
 /* No comment provided by engineer. */
 "Resize Console Command" = "调整控制台大小指令";
@@ -2096,7 +2096,7 @@
 "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 吗?";
+"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" = "分辨率";
@@ -2126,7 +2126,7 @@
 "Run Recovery" = "运行 Recovery 模式";
 
 /* No comment provided by engineer. */
-"Run selected VM" = "运行已选的虚拟机";
+"Run selected VM" = "运行已选的虚拟机";
 
 /* No comment provided by engineer. */
 "Run the VM in the foreground." = "在前台运行虚拟机。";
@@ -2147,7 +2147,7 @@
 "Select an existing disk image." = "选择一个现有的磁盘映像。";
 
 /* No comment provided by engineer. */
-"Serial" = "序列";
+"Serial" = "串行";
 
 /* No comment provided by engineer. */
 "Server Address" = "服务器地址";
@@ -2174,7 +2174,7 @@
 "Share is read only" = "共享为只读";
 
 /* No comment provided by engineer. */
-"Share selected VM" = "共享已选的虚拟机";
+"Share selected VM" = "共享已选的虚拟机";
 
 /* No comment provided by engineer. */
 "Shared Directory Path" = "共享目录路径";
@@ -2225,7 +2225,7 @@
 "Status" = "状态";
 
 /* No comment provided by engineer. */
-"Stop selected VM" = "停止已选的虚拟机";
+"Stop selected VM" = "停止已选的虚拟机";
 
 /* No comment provided by engineer. */
 "Stop the running VM." = "停止正在运行的虚拟机。";
@@ -2267,10 +2267,10 @@
 "This is appended to the -machine argument." = "这会添加到 -machine 参数的末端。";
 
 /* No comment provided by engineer. */
-"This virtual machine cannot be found at: %@" = "虚拟机无法从此路径中找到:%@";
+"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 中。你可以从此路径中找到:%@";
+"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 设备";
@@ -2349,7 +2349,7 @@
 "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." = "若启动选项已损坏,或者希望重新注册安全启动的默认密钥,可以使用此功能。";
+"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" = "缩放";

+ 1 - 1
QEMUHelper/zh-Hans.lproj/InfoPlist.strings

@@ -5,5 +5,5 @@
 "CFBundleName" = "QEMUHelper";
 
 /* Copyright (human-readable) */
-"NSHumanReadableCopyright" = "版权所有 © 2020 osy。保留所有权利。";
+"NSHumanReadableCopyright" = "Copyright © 2020 osy. 保留所有权利。";
 

+ 1 - 1
README.zh-Hans.md

@@ -20,7 +20,7 @@ UTM 是一个功能齐全的系统模拟器和虚拟机主机,适用于 iOS 
 * 文本终端模式
 * USB 设备
 * 使用 QEMU TCG 进行基于 JIT 的加速
-* 采用了最新最好的 API,从零开始设计前端,支持 macOS 11+ 和 iOS 11+
+* 采用了最新最好的 API,从零开始设计前端,支持 macOS 11+ 和 iOS 11+
 * 直接从你的设备上创建、管理和运行虚拟机
 
 ## macOS 的附加功能

+ 9 - 0
Scripting/UTM.sdef

@@ -103,6 +103,15 @@
             <result type="specifier" description="The new virtual machine (as a specifier)."/>
         </command>
         
+        <command name="export" code="coreexpo" description="Export a virtual machine to a specified location.">
+            <cocoa class="UTMScriptingExportCommand"/>
+            <access-group identifier="*"/>
+            <direct-parameter type="virtual machine" requires-access="r" description="The virtual machine to export."/>
+            <parameter name="to" code="efil" type="file" description="Location to export the VM to.">
+                <cocoa key="file"/>
+            </parameter>
+        </command>
+        
         <class name="virtual machine" code="UTMv" description="A virtual machine registered in UTM." plural="virtual machines">
           <cocoa class="UTMScriptingVirtualMachineImpl"/>
 

+ 1 - 0
Scripting/UTMScripting.swift

@@ -209,6 +209,7 @@ extension SBObject: UTMScriptingWindow {}
     @objc optional func stopBy(_ by: UTMScriptingStopMethod) // Shuts down a running virtual machine.
     @objc optional func delete() // Delete a virtual machine. All data will be deleted, there is no confirmation!
     @objc optional func duplicateWithProperties(_ withProperties: [AnyHashable : Any]!) // Copy an virtual machine and all its data.
+    @objc optional func exportTo(_ to: URL!) // Export a virtual machine to a specified location.
     @objc optional func openFileAt(_ at: String!, for for_: UTMScriptingOpenMode, updating: Bool) -> UTMScriptingGuestFile // Open a file on the guest. You must close the file when you are done to prevent leaking guest resources.
     @objc optional func executeAt(_ at: String!, withArguments: [String]!, withEnvironment: [String]!, usingInput: String!, base64Encoding: Bool, outputCapturing: Bool) -> UTMScriptingGuestProcess // Execute a command or script on the guest.
     @objc optional func queryIp() -> [Any] // Query the guest for all IP addresses on its network interfaces (excluding loopback).

+ 31 - 0
Scripting/UTMScriptingExportCommand.swift

@@ -0,0 +1,31 @@
+//
+// Copyright © 2024 naveenrajm7. 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
+
+@MainActor
+
+@objc(UTMScriptingExportCommand)
+class UTMScriptingExportCommand: NSCloneCommand, UTMScriptable {
+    override func performDefaultImplementation() -> Any? {
+        if let scriptingVM = keySpecifier.objectsByEvaluatingSpecifier as? UTMScriptingVirtualMachineImpl {
+            scriptingVM.export(self)
+            return nil
+        } else {
+            return super.performDefaultImplementation()
+        }
+    }
+}

+ 10 - 0
Scripting/UTMScriptingVirtualMachineImpl.swift

@@ -180,6 +180,16 @@ class UTMScriptingVirtualMachineImpl: NSObject, UTMScriptable {
             }
         }
     }
+    
+    @objc func export(_ command: NSCloneCommand) {
+        let exportUrl = command.evaluatedArguments?["file"] as? URL
+        withScriptCommand(command) { [self] in
+            guard vm.state == .stopped else {
+                throw ScriptingError.notStopped
+            }
+            try await data.export(vm: box, to: exportUrl!)
+        }
+    }
 }
 
 // MARK: - Guest agent suite

+ 168 - 8
Services/UTMAppleVirtualMachine.swift

@@ -114,8 +114,10 @@ final class UTMAppleVirtualMachine: UTMVirtualMachine {
     
     weak var screenshotDelegate: UTMScreenshotProvider?
     
-    private var activeResourceUrls: [URL] = []
-    
+    private var activeResourceUrls: [String: URL] = [:]
+
+    private var removableDrives: [String: Any] = [:]
+
     @MainActor required init(packageUrl: URL, configuration: UTMAppleConfiguration, isShortcut: Bool = false) throws {
         self.isScopedAccess = packageUrl.startAccessingSecurityScopedResource()
         // load configuration
@@ -187,14 +189,18 @@ final class UTMAppleVirtualMachine: UTMVirtualMachine {
             } else {
                 try await _start(options: options)
             }
+            if #available(macOS 15, *) {
+                try await attachExternalDrives()
+            }
             if #available(macOS 12, *) {
                 Task { @MainActor in
+                    let tag = config.shareDirectoryTag
                     sharedDirectoriesChanged = config.sharedDirectoriesPublisher.sink { [weak self] newShares in
                         guard let self = self else {
                             return
                         }
                         self.vmQueue.async {
-                            self.updateSharedDirectories(with: newShares)
+                            self.updateSharedDirectories(with: newShares, tag: tag)
                         }
                     }
                 }
@@ -516,10 +522,10 @@ final class UTMAppleVirtualMachine: UTMVirtualMachine {
     }
     
     @available(macOS 12, *)
-    private func updateSharedDirectories(with newShares: [UTMAppleConfigurationSharedDirectory]) {
+    private func updateSharedDirectories(with newShares: [UTMAppleConfigurationSharedDirectory], tag: String) {
         guard let fsConfig = apple?.directorySharingDevices.first(where: { device in
             if let device = device as? VZVirtioFileSystemDevice {
-                return device.tag == "share"
+                return device.tag == tag
             } else {
                 return false
             }
@@ -614,7 +620,7 @@ final class UTMAppleVirtualMachine: UTMVirtualMachine {
             let drive = config.drives[i]
             if let url = drive.imageURL, drive.isExternal {
                 if url.startAccessingSecurityScopedResource() {
-                    activeResourceUrls.append(url)
+                    activeResourceUrls[drive.id] = url
                 } else {
                     config.drives[i].imageURL = nil
                     throw UTMAppleVirtualMachineError.cannotAccessResource(url)
@@ -625,7 +631,7 @@ final class UTMAppleVirtualMachine: UTMVirtualMachine {
             let share = config.sharedDirectories[i]
             if let url = share.directoryURL {
                 if url.startAccessingSecurityScopedResource() {
-                    activeResourceUrls.append(url)
+                    activeResourceUrls[share.id.uuidString] = url
                 } else {
                     config.sharedDirectories[i].directoryURL = nil
                     throw UTMAppleVirtualMachineError.cannotAccessResource(url)
@@ -635,7 +641,7 @@ final class UTMAppleVirtualMachine: UTMVirtualMachine {
     }
     
     @MainActor private func stopAccesingResources() {
-        for url in activeResourceUrls {
+        for url in activeResourceUrls.values {
             url.stopAccessingSecurityScopedResource()
         }
         activeResourceUrls.removeAll()
@@ -649,6 +655,7 @@ extension UTMAppleVirtualMachine: VZVirtualMachineDelegate {
             apple = nil
             snapshotUnsupportedError = nil
         }
+        removableDrives.removeAll()
         sharedDirectoriesChanged = nil
         Task { @MainActor in
             stopAccesingResources()
@@ -731,6 +738,159 @@ extension UTMAppleVirtualMachine: VZVirtualMachineDelegate {
     }
 }
 
+@available(macOS 15, *)
+extension UTMAppleVirtualMachine {
+    private func detachDrive(id: String) async throws {
+        if let oldUrl = activeResourceUrls.removeValue(forKey: id) {
+            oldUrl.stopAccessingSecurityScopedResource()
+        }
+        if let device = removableDrives.removeValue(forKey: id) as? any VZUSBDevice {
+            try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, any Error>) in
+                vmQueue.async {
+                    guard let apple = self.apple, let usbController = apple.usbControllers.first else {
+                        continuation.resume(throwing: UTMAppleVirtualMachineError.operationNotAvailable)
+                        return
+                    }
+                    usbController.detach(device: device) { error in
+                        if let error = error {
+                            continuation.resume(throwing: error)
+                        } else {
+                            continuation.resume()
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    /// Eject a removable drive
+    /// - Parameter drive: Removable drive
+    func eject(_ drive: UTMAppleConfigurationDrive) async throws {
+        if state == .started {
+            try await detachDrive(id: drive.id)
+        }
+        await registryEntry.removeExternalDrive(forId: drive.id)
+    }
+
+    private func attachDrive(_ drive: VZDiskImageStorageDeviceAttachment, imageURL: URL, id: String) async throws {
+        if imageURL.startAccessingSecurityScopedResource() {
+            activeResourceUrls[id] = imageURL
+        }
+        let configuration = VZUSBMassStorageDeviceConfiguration(attachment: drive)
+        let device = VZUSBMassStorageDevice(configuration: configuration)
+        try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, any Error>) in
+            vmQueue.async {
+                guard let apple = self.apple, let usbController = apple.usbControllers.first else {
+                    continuation.resume(throwing: UTMAppleVirtualMachineError.operationNotAvailable)
+                    return
+                }
+                usbController.attach(device: device) { error in
+                    if let error = error {
+                        continuation.resume(throwing: error)
+                    } else {
+                        continuation.resume()
+                    }
+                }
+            }
+        }
+        removableDrives[id] = device
+    }
+
+    /// Change mount image of a removable drive
+    /// - Parameters:
+    ///   - drive: Removable drive
+    ///   - url: New mount image
+    func changeMedium(_ drive: UTMAppleConfigurationDrive, to url: URL) async throws {
+        var newDrive = drive
+        newDrive.imageURL = url
+        let scopedAccess = url.startAccessingSecurityScopedResource()
+        defer {
+            if scopedAccess {
+                url.stopAccessingSecurityScopedResource()
+            }
+        }
+        let attachment = try newDrive.vzDiskImage()!
+        if state == .started {
+            try await detachDrive(id: drive.id)
+            try await attachDrive(attachment, imageURL: url, id: drive.id)
+        }
+        let file = try UTMRegistryEntry.File(url: url)
+        await registryEntry.setExternalDrive(file, forId: drive.id)
+    }
+
+    private func _attachExternalDrives(_ drives: [any VZUSBDevice]) -> (any Error)? {
+        let group = DispatchGroup()
+        var lastError: (any Error)?
+        group.enter()
+        vmQueue.async {
+            defer {
+                group.leave()
+            }
+            guard let apple = self.apple, let usbController = apple.usbControllers.first else {
+                lastError = UTMAppleVirtualMachineError.operationNotAvailable
+                return
+            }
+            for device in drives {
+                group.enter()
+                usbController.attach(device: device) { error in
+                    if let error = error {
+                        lastError = error
+                    }
+                    group.leave()
+                }
+            }
+        }
+        group.wait()
+        return lastError
+    }
+
+    private func attachExternalDrives() async throws {
+        let removableDrives = try await config.drives.reduce(into: [String: any VZUSBDevice]()) { devices, drive in
+            guard drive.isExternal else {
+                return
+            }
+            guard let attachment = try drive.vzDiskImage() else {
+                return
+            }
+            let configuration = VZUSBMassStorageDeviceConfiguration(attachment: attachment)
+            devices[drive.id] = VZUSBMassStorageDevice(configuration: configuration)
+        }
+        let drives = Array(removableDrives.values)
+        try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, any Error>) in
+            if let error = self._attachExternalDrives(drives) {
+                continuation.resume(throwing: error)
+            } else {
+                continuation.resume()
+            }
+        }
+        self.removableDrives = removableDrives
+    }
+
+    private var guestToolsId: String {
+        "guest-tools"
+    }
+
+    var hasGuestToolsAttached: Bool {
+        removableDrives.keys.contains(guestToolsId)
+    }
+
+    func attachGuestTools(_ imageURL: URL) async throws {
+        try await detachDrive(id: guestToolsId)
+        let scopedAccess = imageURL.startAccessingSecurityScopedResource()
+        defer {
+            if scopedAccess {
+                imageURL.stopAccessingSecurityScopedResource()
+            }
+        }
+        let attachment = try VZDiskImageStorageDeviceAttachment(url: imageURL, readOnly: true)
+        try await attachDrive(attachment, imageURL: imageURL, id: guestToolsId)
+    }
+
+    func detachGuestTools() async throws {
+        try await detachDrive(id: guestToolsId)
+    }
+}
+
 protocol UTMScreenshotProvider: AnyObject {
     var screenshot: UTMVirtualMachineScreenshot? { get }
 }

+ 44 - 2
Services/UTMRegistryEntry.swift

@@ -36,7 +36,9 @@ import Combine
     @Published private var _windowSettings: [Int: Window]
     
     @Published private var _terminalSettings: [Int: Terminal]
-    
+
+    @Published private var _resolutionSettings: [Int: Resolution]
+
     @Published private var _hasMigratedConfig: Bool
     
     @Published private var _macRecoveryIpsw: File?
@@ -50,6 +52,7 @@ import Combine
         case sharedDirectories = "SharedDirectories"
         case windowSettings = "WindowSettings"
         case terminalSettings = "TerminalSettings"
+        case resolutionSettings = "ResolutionSettings"
         case hasMigratedConfig = "MigratedConfig"
         case macRecoveryIpsw = "MacRecoveryIpsw"
     }
@@ -69,6 +72,7 @@ import Combine
         _sharedDirectories = []
         _windowSettings = [:]
         _terminalSettings = [:]
+        _resolutionSettings = [:]
         _hasMigratedConfig = false
     }
     
@@ -89,6 +93,7 @@ import Combine
         _sharedDirectories = try container.decode([File].self, forKey: .sharedDirectories).filter({ $0.isValid })
         _windowSettings = try container.decode([Int: Window].self, forKey: .windowSettings)
         _terminalSettings = try container.decodeIfPresent([Int: Terminal].self, forKey: .terminalSettings) ?? [:]
+        _resolutionSettings = try container.decodeIfPresent([Int: Resolution].self, forKey: .resolutionSettings) ?? [:]
         _hasMigratedConfig = try container.decodeIfPresent(Bool.self, forKey: .hasMigratedConfig) ?? false
         _macRecoveryIpsw = try container.decodeIfPresent(File.self, forKey: .macRecoveryIpsw)
     }
@@ -103,6 +108,7 @@ import Combine
         try container.encode(_sharedDirectories, forKey: .sharedDirectories)
         try container.encode(_windowSettings, forKey: .windowSettings)
         try container.encode(_terminalSettings, forKey: .terminalSettings)
+        try container.encode(_resolutionSettings, forKey: .resolutionSettings)
         if _hasMigratedConfig {
             try container.encode(_hasMigratedConfig, forKey: .hasMigratedConfig)
         }
@@ -201,7 +207,17 @@ extension UTMRegistryEntry: UTMRegistryEntryDecodable {}
             _terminalSettings = newValue
         }
     }
-    
+
+    var resolutionSettings: [Int: Resolution] {
+        get {
+            _resolutionSettings
+        }
+
+        set {
+            _resolutionSettings = newValue
+        }
+    }
+
     var hasMigratedConfig: Bool {
         get {
             _hasMigratedConfig
@@ -254,6 +270,7 @@ extension UTMRegistryEntry: UTMRegistryEntryDecodable {}
         sharedDirectories = other.sharedDirectories
         windowSettings = other.windowSettings
         terminalSettings = other.terminalSettings
+        resolutionSettings = other.resolutionSettings
         hasMigratedConfig = other.hasMigratedConfig
     }
     
@@ -493,4 +510,29 @@ extension UTMRegistryEntry {
             try container.encode(rows, forKey: .rows)
         }
     }
+
+    struct Resolution: Codable, Equatable {
+        var size: CGSize = .zero
+
+        var isFullscreen: Bool = false
+
+        private enum CodingKeys: String, CodingKey {
+            case size = "Size"
+            case isFullscreen = "Fullscreen"
+        }
+
+        init() {}
+
+        init(from decoder: Decoder) throws {
+            let container = try decoder.container(keyedBy: CodingKeys.self)
+            size = try container.decode(CGSize.self, forKey: .size)
+            isFullscreen = try container.decode(Bool.self, forKey: .isFullscreen)
+        }
+
+        func encode(to encoder: Encoder) throws {
+            var container = encoder.container(keyedBy: CodingKeys.self)
+            try container.encode(size, forKey: .size)
+            try container.encode(isFullscreen, forKey: .isFullscreen)
+        }
+    }
 }

+ 44 - 18
UTM.xcodeproj/project.pbxproj

@@ -271,6 +271,7 @@
 		B329049C270FE136002707AC /* AltKit in Frameworks */ = {isa = PBXBuildFile; productRef = B329049B270FE136002707AC /* AltKit */; };
 		B3DDF57226E9BBA300CE47F0 /* AltKit in Frameworks */ = {isa = PBXBuildFile; productRef = B3DDF57126E9BBA300CE47F0 /* AltKit */; };
 		CD77BE442CB38F060074ADD2 /* UTMScriptingImportCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD77BE432CB38F060074ADD2 /* UTMScriptingImportCommand.swift */; };
+		CD77BE422CAB51B40074ADD2 /* UTMScriptingExportCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD77BE412CAB519F0074ADD2 /* UTMScriptingExportCommand.swift */; };
 		CE020BA324AEDC7C00B44AB6 /* UTMData.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE020BA224AEDC7C00B44AB6 /* UTMData.swift */; };
 		CE020BA424AEDC7C00B44AB6 /* UTMData.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE020BA224AEDC7C00B44AB6 /* UTMData.swift */; };
 		CE020BA724AEDEF000B44AB6 /* Logging in Frameworks */ = {isa = PBXBuildFile; productRef = CE020BA624AEDEF000B44AB6 /* Logging */; };
@@ -345,7 +346,7 @@
 		CE0B6ECC24AD677200FE012D /* gstriff-1.0.0.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE2D63DE22653C7400FC7E63 /* gstriff-1.0.0.framework */; };
 		CE0B6ECD24AD677200FE012D /* gsttag-1.0.0.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE2D63F622653C7400FC7E63 /* gsttag-1.0.0.framework */; };
 		CE0B6ECF24AD677200FE012D /* gstrtp-1.0.0.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE2D63DD22653C7400FC7E63 /* gstrtp-1.0.0.framework */; };
-		CE0B6ED124AD677200FE012D /* phodav-2.0.0.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE059DC0243BD67100338317 /* phodav-2.0.0.framework */; };
+		CE0B6ED124AD677200FE012D /* phodav-3.0.0.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE059DC0243BD67100338317 /* phodav-3.0.0.framework */; };
 		CE0B6ED324AD677200FE012D /* libgstvideoconvert.a in Frameworks */ = {isa = PBXBuildFile; fileRef = CE9D19542265425900355E14 /* libgstvideoconvert.a */; };
 		CE0B6ED724AD677200FE012D /* gstaudio-1.0.0.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE2D63EF22653C7400FC7E63 /* gstaudio-1.0.0.framework */; };
 		CE0B6EDC24AD677200FE012D /* libgstapp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = CE9D19612265425900355E14 /* libgstapp.a */; };
@@ -391,7 +392,7 @@
 		CE0B6F2924AD67AD00FE012D /* gsttag-1.0.0.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE2D63F622653C7400FC7E63 /* gsttag-1.0.0.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
 		CE0B6F2A24AD67AF00FE012D /* gstvideo-1.0.0.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE2D63F922653C7400FC7E63 /* gstvideo-1.0.0.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
 		CE0B6F2F24AD67BE00FE012D /* json-glib-1.0.0.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE2D63E222653C7400FC7E63 /* json-glib-1.0.0.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
-		CE0B6F3124AD67C100FE012D /* phodav-2.0.0.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE059DC0243BD67100338317 /* phodav-2.0.0.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
+		CE0B6F3124AD67C100FE012D /* phodav-3.0.0.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE059DC0243BD67100338317 /* phodav-3.0.0.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
 		CE0B6F5424AD67FA00FE012D /* spice-client-glib-2.0.8.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE2D63FE22653C7500FC7E63 /* spice-client-glib-2.0.8.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
 		CE0DF19425A83C1700A51894 /* qemu-aarch64-softmmu.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE2D63FD22653C7500FC7E63 /* qemu-aarch64-softmmu.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
 		CE0DF19525A83C1700A51894 /* qemu-alpha-softmmu.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE2D641322653C7500FC7E63 /* qemu-alpha-softmmu.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
@@ -496,7 +497,7 @@
 		CE2D934C24AD46670059923A /* ffi.7.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE2D63E322653C7400FC7E63 /* ffi.7.framework */; };
 		CE2D934D24AD46670059923A /* gstnet-1.0.0.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE2D63E522653C7400FC7E63 /* gstnet-1.0.0.framework */; };
 		CE2D934E24AD46670059923A /* gstbase-1.0.0.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE2D63E822653C7400FC7E63 /* gstbase-1.0.0.framework */; };
-		CE2D934F24AD46670059923A /* phodav-2.0.0.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE059DC0243BD67100338317 /* phodav-2.0.0.framework */; };
+		CE2D934F24AD46670059923A /* phodav-3.0.0.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE059DC0243BD67100338317 /* phodav-3.0.0.framework */; };
 		CE2D935024AD46670059923A /* gstcontroller-1.0.0.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE2D63EE22653C7400FC7E63 /* gstcontroller-1.0.0.framework */; };
 		CE2D935124AD46670059923A /* gstaudio-1.0.0.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE2D63EF22653C7400FC7E63 /* gstaudio-1.0.0.framework */; };
 		CE2D935224AD46670059923A /* gpg-error.0.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE2D63F122653C7400FC7E63 /* gpg-error.0.framework */; };
@@ -544,7 +545,7 @@
 		CE2D938A24AD46670059923A /* gstrtp-1.0.0.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE2D63DD22653C7400FC7E63 /* gstrtp-1.0.0.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
 		CE2D938B24AD46670059923A /* gstriff-1.0.0.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE2D63DE22653C7400FC7E63 /* gstriff-1.0.0.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
 		CE2D938C24AD46670059923A /* qemu-ppc-softmmu.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE2D63E722653C7400FC7E63 /* qemu-ppc-softmmu.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
-		CE2D938D24AD46670059923A /* phodav-2.0.0.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE059DC0243BD67100338317 /* phodav-2.0.0.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
+		CE2D938D24AD46670059923A /* phodav-3.0.0.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE059DC0243BD67100338317 /* phodav-3.0.0.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
 		CE2D938E24AD46670059923A /* gthread-2.0.0.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE2D63DC22653C7300FC7E63 /* gthread-2.0.0.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
 		CE2D938F24AD46670059923A /* qemu-aarch64-softmmu.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE2D63FD22653C7500FC7E63 /* qemu-aarch64-softmmu.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
 		CE2D939024AD46670059923A /* qemu-mips-softmmu.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE2D63FF22653C7500FC7E63 /* qemu-mips-softmmu.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
@@ -771,7 +772,7 @@
 		CEA45F44263519B5002FA97D /* gstnet-1.0.0.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE2D63E522653C7400FC7E63 /* gstnet-1.0.0.framework */; };
 		CEA45F45263519B5002FA97D /* gstbase-1.0.0.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE2D63E822653C7400FC7E63 /* gstbase-1.0.0.framework */; };
 		CEA45F46263519B5002FA97D /* Logging in Frameworks */ = {isa = PBXBuildFile; productRef = CEA45E20263519B5002FA97D /* Logging */; };
-		CEA45F47263519B5002FA97D /* phodav-2.0.0.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE059DC0243BD67100338317 /* phodav-2.0.0.framework */; };
+		CEA45F47263519B5002FA97D /* phodav-3.0.0.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE059DC0243BD67100338317 /* phodav-3.0.0.framework */; };
 		CEA45F49263519B5002FA97D /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE0E9B86252FD06B0026E02B /* SwiftUI.framework */; settings = {ATTRIBUTES = (Weak, ); }; };
 		CEA45F4A263519B5002FA97D /* gstcontroller-1.0.0.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE2D63EE22653C7400FC7E63 /* gstcontroller-1.0.0.framework */; };
 		CEA45F4B263519B5002FA97D /* gstaudio-1.0.0.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE2D63EF22653C7400FC7E63 /* gstaudio-1.0.0.framework */; };
@@ -817,7 +818,7 @@
 		CEA45F86263519B5002FA97D /* gstrtp-1.0.0.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE2D63DD22653C7400FC7E63 /* gstrtp-1.0.0.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
 		CEA45F87263519B5002FA97D /* gstriff-1.0.0.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE2D63DE22653C7400FC7E63 /* gstriff-1.0.0.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
 		CEA45F88263519B5002FA97D /* qemu-ppc-softmmu.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE2D63E722653C7400FC7E63 /* qemu-ppc-softmmu.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
-		CEA45F89263519B5002FA97D /* phodav-2.0.0.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE059DC0243BD67100338317 /* phodav-2.0.0.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
+		CEA45F89263519B5002FA97D /* phodav-3.0.0.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE059DC0243BD67100338317 /* phodav-3.0.0.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
 		CEA45F8A263519B5002FA97D /* gthread-2.0.0.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE2D63DC22653C7300FC7E63 /* gthread-2.0.0.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
 		CEA45F8B263519B5002FA97D /* qemu-aarch64-softmmu.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE2D63FD22653C7500FC7E63 /* qemu-aarch64-softmmu.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
 		CEA45F8F263519B5002FA97D /* gobject-2.0.0.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE2D63F522653C7400FC7E63 /* gobject-2.0.0.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
@@ -891,6 +892,14 @@
 		CEC794BD2949663C00121A9F /* UTMScripting.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEC794BB2949663C00121A9F /* UTMScripting.swift */; };
 		CED234ED254796E500ED0A57 /* NumberTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED234EC254796E500ED0A57 /* NumberTextField.swift */; };
 		CED234EE254796E500ED0A57 /* NumberTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED234EC254796E500ED0A57 /* NumberTextField.swift */; };
+		CED297142CE425CB00F1E3EB /* soup-3.0.0.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CED297132CE425CA00F1E3EB /* soup-3.0.0.framework */; };
+		CED297152CE425CB00F1E3EB /* soup-3.0.0.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CED297132CE425CA00F1E3EB /* soup-3.0.0.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+		CED297192CE4263100F1E3EB /* soup-3.0.0.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CED297132CE425CA00F1E3EB /* soup-3.0.0.framework */; };
+		CED2971A2CE4263100F1E3EB /* soup-3.0.0.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CED297132CE425CA00F1E3EB /* soup-3.0.0.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+		CED2971B2CE4263600F1E3EB /* soup-3.0.0.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CED297132CE425CA00F1E3EB /* soup-3.0.0.framework */; };
+		CED2971C2CE4263600F1E3EB /* soup-3.0.0.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CED297132CE425CA00F1E3EB /* soup-3.0.0.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+		CED2971D2CE4263A00F1E3EB /* soup-3.0.0.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CED297132CE425CA00F1E3EB /* soup-3.0.0.framework */; };
+		CED2971E2CE4263A00F1E3EB /* soup-3.0.0.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CED297132CE425CA00F1E3EB /* soup-3.0.0.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
 		CED779E52C78C82A00EB82AE /* UTMTips.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED779E42C78C82A00EB82AE /* UTMTips.swift */; };
 		CED779E62C78C82A00EB82AE /* UTMTips.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED779E42C78C82A00EB82AE /* UTMTips.swift */; };
 		CED779E72C78C82A00EB82AE /* UTMTips.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED779E42C78C82A00EB82AE /* UTMTips.swift */; };
@@ -915,6 +924,7 @@
 		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 */; };
+		CEEF26A72CEDAEEA003F7B8C /* UTMDownloadMacSupportToolsTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEEF26A62CEDAEEA003F7B8C /* UTMDownloadMacSupportToolsTask.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 */; };
@@ -1135,7 +1145,7 @@
 		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 */; };
+		CEF7F65A2AEEDCC400E34952 /* phodav-3.0.0.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE059DC0243BD67100338317 /* phodav-3.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 */; };
@@ -1181,7 +1191,7 @@
 		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, ); }; };
+		CEF7F69C2AEEDCC400E34952 /* phodav-3.0.0.framework in Embed Libraries */ = {isa = PBXBuildFile; fileRef = CE059DC0243BD67100338317 /* phodav-3.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, ); }; };
@@ -1289,6 +1299,7 @@
 				CE0B6F1F24AD679E00FE012D /* gstcontroller-1.0.0.framework in Embed Libraries */,
 				CEF83F8E250094EC00557D15 /* gpg-error.0.framework in Embed Libraries */,
 				CE0B6F1A24AD679500FE012D /* gstallocators-1.0.0.framework in Embed Libraries */,
+				CED297152CE425CB00F1E3EB /* soup-3.0.0.framework in Embed Libraries */,
 				CE03D08F24D9124200F76B84 /* gobject-2.0.0.framework in Embed Libraries */,
 				CE0B6F1D24AD679B00FE012D /* gstbase-1.0.0.framework in Embed Libraries */,
 				CE0B6F2224AD67A200FE012D /* gstnet-1.0.0.framework in Embed Libraries */,
@@ -1300,7 +1311,7 @@
 				CEF83F8B250094D700557D15 /* spice-server.1.framework in Embed Libraries */,
 				CE0B6F1B24AD679700FE012D /* gstapp-1.0.0.framework in Embed Libraries */,
 				CE0B6F2924AD67AD00FE012D /* gsttag-1.0.0.framework in Embed Libraries */,
-				CE0B6F3124AD67C100FE012D /* phodav-2.0.0.framework in Embed Libraries */,
+				CE0B6F3124AD67C100FE012D /* phodav-3.0.0.framework in Embed Libraries */,
 				CE0B6F2624AD67A900FE012D /* gstrtp-1.0.0.framework in Embed Libraries */,
 				CE0B6F2524AD67A700FE012D /* gstriff-1.0.0.framework in Embed Libraries */,
 				CE0B6F2424AD67A600FE012D /* gstreamer-1.0.0.framework in Embed Libraries */,
@@ -1396,7 +1407,7 @@
 				CE2D938B24AD46670059923A /* gstriff-1.0.0.framework in Embed Libraries */,
 				CE2D938C24AD46670059923A /* qemu-ppc-softmmu.framework in Embed Libraries */,
 				84C5068728CA5702007CE8FF /* Hypervisor.framework in Embed Libraries */,
-				CE2D938D24AD46670059923A /* phodav-2.0.0.framework in Embed Libraries */,
+				CE2D938D24AD46670059923A /* phodav-3.0.0.framework in Embed Libraries */,
 				CE2D938E24AD46670059923A /* gthread-2.0.0.framework in Embed Libraries */,
 				CE2D938F24AD46670059923A /* qemu-aarch64-softmmu.framework in Embed Libraries */,
 				CEA9059225FC6A3500801E7C /* usbredirparser.1.framework in Embed Libraries */,
@@ -1426,6 +1437,7 @@
 				CE2D93A324AD46670059923A /* gstvideo-1.0.0.framework in Embed Libraries */,
 				CE2D93A424AD46670059923A /* json-glib-1.0.0.framework in Embed Libraries */,
 				CE2D93A524AD46670059923A /* pixman-1.0.framework in Embed Libraries */,
+				CED2971A2CE4263100F1E3EB /* soup-3.0.0.framework in Embed Libraries */,
 				CE2D93A624AD46670059923A /* jpeg.62.framework in Embed Libraries */,
 				CE2D93A724AD46670059923A /* qemu-microblazeel-softmmu.framework in Embed Libraries */,
 				CE2D93A824AD46670059923A /* qemu-hppa-softmmu.framework in Embed Libraries */,
@@ -1479,12 +1491,13 @@
 				CEA45F7C263519B5002FA97D /* gstnet-1.0.0.framework in Embed Libraries */,
 				CEA45F7E263519B5002FA97D /* crypto.1.1.framework in Embed Libraries */,
 				CEA45F7F263519B5002FA97D /* qemu-riscv64-softmmu.framework in Embed Libraries */,
+				CED2971C2CE4263600F1E3EB /* soup-3.0.0.framework in Embed Libraries */,
 				CEA45F80263519B5002FA97D /* gstapp-1.0.0.framework in Embed Libraries */,
 				CEA45F84263519B5002FA97D /* gsttag-1.0.0.framework in Embed Libraries */,
 				CEA45F86263519B5002FA97D /* gstrtp-1.0.0.framework in Embed Libraries */,
 				CEA45F87263519B5002FA97D /* gstriff-1.0.0.framework in Embed Libraries */,
 				CEA45F88263519B5002FA97D /* qemu-ppc-softmmu.framework in Embed Libraries */,
-				CEA45F89263519B5002FA97D /* phodav-2.0.0.framework in Embed Libraries */,
+				CEA45F89263519B5002FA97D /* phodav-3.0.0.framework in Embed Libraries */,
 				CEA45F8A263519B5002FA97D /* gthread-2.0.0.framework in Embed Libraries */,
 				84937F20289767F0003148F4 /* zstd.1.framework in Embed Libraries */,
 				CEA45F8B263519B5002FA97D /* qemu-aarch64-softmmu.framework in Embed Libraries */,
@@ -1543,6 +1556,7 @@
 				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 */,
+				CED2971E2CE4263A00F1E3EB /* soup-3.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 */,
@@ -1554,7 +1568,7 @@
 				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 */,
+				CEF7F69C2AEEDCC400E34952 /* phodav-3.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 */,
@@ -1764,6 +1778,7 @@
 		C03453B02709E35200AD51AD /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = "<group>"; };
 		C8958B6D243634DA002D86B4 /* ko */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ko; path = ko.lproj/Localizable.strings; sourceTree = "<group>"; };
 		CD77BE432CB38F060074ADD2 /* UTMScriptingImportCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMScriptingImportCommand.swift; sourceTree = "<group>"; };
+		CD77BE412CAB519F0074ADD2 /* UTMScriptingExportCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMScriptingExportCommand.swift; sourceTree = "<group>"; };
 		CE020BA224AEDC7C00B44AB6 /* UTMData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMData.swift; sourceTree = "<group>"; };
 		CE020BAA24AEE00000B44AB6 /* UTMLoggingSwift.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMLoggingSwift.swift; sourceTree = "<group>"; };
 		CE020BB524B14F8400B44AB6 /* UTMVirtualMachine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMVirtualMachine.swift; sourceTree = "<group>"; };
@@ -1776,7 +1791,7 @@
 		CE03D0D324DCF6DD00F76B84 /* VMMetalViewInputDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VMMetalViewInputDelegate.swift; sourceTree = "<group>"; };
 		CE056CA4242454100004B68A /* VMDisplayMetalViewController+Touch.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "VMDisplayMetalViewController+Touch.h"; sourceTree = "<group>"; };
 		CE056CA5242454100004B68A /* VMDisplayMetalViewController+Touch.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "VMDisplayMetalViewController+Touch.m"; sourceTree = "<group>"; };
-		CE059DC0243BD67100338317 /* phodav-2.0.0.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = "phodav-2.0.0.framework"; path = "$(SYSROOT_DIR)/Frameworks/phodav-2.0.0.framework"; sourceTree = "<group>"; };
+		CE059DC0243BD67100338317 /* phodav-3.0.0.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = "phodav-3.0.0.framework"; path = "$(SYSROOT_DIR)/Frameworks/phodav-3.0.0.framework"; sourceTree = "<group>"; };
 		CE059DC3243BFA3200338317 /* UTMLegacyQemuConfiguration+Sharing.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UTMLegacyQemuConfiguration+Sharing.h"; sourceTree = "<group>"; };
 		CE059DC4243BFA3200338317 /* UTMLegacyQemuConfiguration+Sharing.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UTMLegacyQemuConfiguration+Sharing.m"; sourceTree = "<group>"; };
 		CE059DC6243E9E3400338317 /* UTMLocationManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = UTMLocationManager.h; sourceTree = "<group>"; };
@@ -2017,6 +2032,7 @@
 		CECF02562B706ADD00409FC0 /* UTMRemoteConnectInterface.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = UTMRemoteConnectInterface.h; sourceTree = "<group>"; };
 		CECF02572B70909900409FC0 /* Info-Remote.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "Info-Remote.plist"; sourceTree = "<group>"; };
 		CED234EC254796E500ED0A57 /* NumberTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NumberTextField.swift; sourceTree = "<group>"; };
+		CED297132CE425CA00F1E3EB /* soup-3.0.0.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = "soup-3.0.0.framework"; path = "$(SYSROOT_DIR)/Frameworks/soup-3.0.0.framework"; sourceTree = "<group>"; };
 		CED779E42C78C82A00EB82AE /* UTMTips.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMTips.swift; sourceTree = "<group>"; };
 		CED779E92C7938D500EB82AE /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/Localizable.strings; sourceTree = "<group>"; };
 		CED814E824C79F070042F0F1 /* VMConfigDriveCreateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VMConfigDriveCreateView.swift; sourceTree = "<group>"; };
@@ -2041,6 +2057,7 @@
 		CEEB66452284B942002737B2 /* VMKeyboardButton.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = VMKeyboardButton.m; sourceTree = "<group>"; };
 		CEEC811A24E48EC600ACB0B3 /* SettingsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
 		CEECE13B25E47D9500A2AAB8 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
+		CEEF26A62CEDAEEA003F7B8C /* UTMDownloadMacSupportToolsTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMDownloadMacSupportToolsTask.swift; sourceTree = "<group>"; };
 		CEF01DB12B6724A300725A0F /* UTMSpiceVirtualMachine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMSpiceVirtualMachine.swift; sourceTree = "<group>"; };
 		CEF01DB62B674BF000725A0F /* UTMPipeInterface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMPipeInterface.swift; sourceTree = "<group>"; };
 		CEF0300526A25A6900667B63 /* VMWizardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VMWizardView.swift; sourceTree = "<group>"; };
@@ -2130,6 +2147,7 @@
 				CE2D933224AD46670059923A /* libgstvolume.a in Frameworks */,
 				CE2D933324AD46670059923A /* libgstcoreelements.a in Frameworks */,
 				CE2D933424AD46670059923A /* libgstvideorate.a in Frameworks */,
+				CED297192CE4263100F1E3EB /* soup-3.0.0.framework in Frameworks */,
 				CE2D933524AD46670059923A /* libgstjpeg.a in Frameworks */,
 				CE2D933624AD46670059923A /* libgstaudioresample.a in Frameworks */,
 				CE2D933724AD46670059923A /* libgstplayback.a in Frameworks */,
@@ -2165,7 +2183,7 @@
 				CE020BA724AEDEF000B44AB6 /* Logging in Frameworks */,
 				8401865A2887AFD50050AC51 /* SwiftTerm in Frameworks */,
 				CE02C8AC294EE4EC006DFE48 /* slirp.0.framework in Frameworks */,
-				CE2D934F24AD46670059923A /* phodav-2.0.0.framework in Frameworks */,
+				CE2D934F24AD46670059923A /* phodav-3.0.0.framework in Frameworks */,
 				CEA9059025FC6A1700801E7C /* usbredirparser.1.framework in Frameworks */,
 				CE0E9B87252FD06B0026E02B /* SwiftUI.framework in Frameworks */,
 				CE2D935024AD46670059923A /* gstcontroller-1.0.0.framework in Frameworks */,
@@ -2232,7 +2250,7 @@
 				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 */,
+				CE0B6ED124AD677200FE012D /* phodav-3.0.0.framework in Frameworks */,
 				CEF83F862500947D00557D15 /* gcrypt.20.framework in Frameworks */,
 				CE0B6ECB24AD677200FE012D /* gstcheck-1.0.0.framework in Frameworks */,
 				CE0B6F0724AD677200FE012D /* libgstvolume.a in Frameworks */,
@@ -2258,6 +2276,7 @@
 				84937EFF28960789003148F4 /* zstd.1.framework in Frameworks */,
 				CE0B6EE224AD677200FE012D /* gstnet-1.0.0.framework in Frameworks */,
 				CE03D08624D90F0700F76B84 /* gmodule-2.0.0.framework in Frameworks */,
+				CED297142CE425CB00F1E3EB /* soup-3.0.0.framework in Frameworks */,
 				CE03D0CA24D9142000F76B84 /* ssl.1.1.framework in Frameworks */,
 				CE0B6EC624AD677200FE012D /* gstfft-1.0.0.framework in Frameworks */,
 				CE0B6EE624AD677200FE012D /* libgstaudiorate.a in Frameworks */,
@@ -2297,6 +2316,7 @@
 				CEA45F2A263519B5002FA97D /* MetalKit.framework in Frameworks */,
 				84CF5DF3288E433F00D01721 /* SwiftUIVisualEffects in Frameworks */,
 				84818C0D2898A07F009EDB67 /* AVFAudio.framework in Frameworks */,
+				CED2971B2CE4263600F1E3EB /* soup-3.0.0.framework in Frameworks */,
 				CEA45F2B263519B5002FA97D /* libgstvolume.a in Frameworks */,
 				CEA45F2C263519B5002FA97D /* libgstcoreelements.a in Frameworks */,
 				CEA45F2D263519B5002FA97D /* libgstvideorate.a in Frameworks */,
@@ -2328,7 +2348,7 @@
 				CEA45F45263519B5002FA97D /* gstbase-1.0.0.framework in Frameworks */,
 				CEA45F46263519B5002FA97D /* Logging in Frameworks */,
 				84A0A88C2A47D5D70038F329 /* QEMUKit in Frameworks */,
-				CEA45F47263519B5002FA97D /* phodav-2.0.0.framework in Frameworks */,
+				CEA45F47263519B5002FA97D /* phodav-3.0.0.framework in Frameworks */,
 				CEA45F49263519B5002FA97D /* SwiftUI.framework in Frameworks */,
 				CEA45F4A263519B5002FA97D /* gstcontroller-1.0.0.framework in Frameworks */,
 				CEA45F4B263519B5002FA97D /* gstaudio-1.0.0.framework in Frameworks */,
@@ -2410,7 +2430,8 @@
 				CEF7F6562AEEDCC400E34952 /* gstbase-1.0.0.framework in Frameworks */,
 				CEF7F6572AEEDCC400E34952 /* Logging in Frameworks */,
 				CEF7F6582AEEDCC400E34952 /* SwiftTerm in Frameworks */,
-				CEF7F65A2AEEDCC400E34952 /* phodav-2.0.0.framework in Frameworks */,
+				CED2971D2CE4263A00F1E3EB /* soup-3.0.0.framework in Frameworks */,
+				CEF7F65A2AEEDCC400E34952 /* phodav-3.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 */,
@@ -2513,6 +2534,7 @@
 		CE2D63D622653C7300FC7E63 /* Frameworks */ = {
 			isa = PBXGroup;
 			children = (
+				CED297132CE425CA00F1E3EB /* soup-3.0.0.framework */,
 				CE064C642A563F4A003C833D /* swtpm.0.framework */,
 				CE02C8A8294EE4EA006DFE48 /* qemu-loongarch64-softmmu.framework */,
 				CE02C8A9294EE4EB006DFE48 /* slirp.0.framework */,
@@ -2529,7 +2551,7 @@
 				CEA9058825FC69D100801E7C /* usbredirparser.1.framework */,
 				CEA9053725F981E900801E7C /* usb-1.0.0.framework */,
 				CE0E9B86252FD06B0026E02B /* SwiftUI.framework */,
-				CE059DC0243BD67100338317 /* phodav-2.0.0.framework */,
+				CE059DC0243BD67100338317 /* phodav-3.0.0.framework */,
 				CE66450C2269313200B0849A /* MetalKit.framework */,
 				CE9D195D2265425900355E14 /* libgstadder.a */,
 				CE9D19612265425900355E14 /* libgstapp.a */,
@@ -2629,6 +2651,7 @@
 				84B36D2427B704C200C22685 /* UTMDownloadVMTask.swift */,
 				844EC0FA2773EE49003C104A /* UTMDownloadIPSWTask.swift */,
 				843232B628C4816100CFBC97 /* UTMDownloadSupportToolsTask.swift */,
+				CEEF26A62CEDAEEA003F7B8C /* UTMDownloadMacSupportToolsTask.swift */,
 				835AA7B026AB7C85007A0411 /* UTMPendingVirtualMachine.swift */,
 				CE611BE629F50CAD001817BC /* UTMReleaseHelper.swift */,
 				847BF9A92A49C783000BD9AA /* VMData.swift */,
@@ -3009,6 +3032,7 @@
 				CD77BE432CB38F060074ADD2 /* UTMScriptingImportCommand.swift */,
 				CE25125029C806AF000790AB /* UTMScriptingDeleteCommand.swift */,
 				CE25125229C80A18000790AB /* UTMScriptingCloneCommand.swift */,
+				CD77BE412CAB519F0074ADD2 /* UTMScriptingExportCommand.swift */,
 			);
 			path = Scripting;
 			sourceTree = "<group>";
@@ -3814,6 +3838,7 @@
 				CEF01DB52B6724A300725A0F /* UTMSpiceVirtualMachine.swift in Sources */,
 				8432329A28C3084A00CFBC97 /* GlobalFileImporter.swift in Sources */,
 				CE19392826DCB094005CEC17 /* RAMSlider.swift in Sources */,
+				CD77BE422CAB51B40074ADD2 /* UTMScriptingExportCommand.swift in Sources */,
 				2C33B3AA2566C9B100A954A6 /* VMContextMenuModifier.swift in Sources */,
 				84BB993A2899E8D500DF28B2 /* VMHeadlessSessionState.swift in Sources */,
 				CE2D955A24AD4F980059923A /* VMToolbarModifier.swift in Sources */,
@@ -3824,6 +3849,7 @@
 				8443EFF42845641600B2E6E2 /* UTMQemuConfigurationDrive.swift in Sources */,
 				CD77BE442CB38F060074ADD2 /* UTMScriptingImportCommand.swift in Sources */,
 				CEFE96772B69A7CC000F00C9 /* VMRemoteSessionState.swift in Sources */,
+				CEEF26A72CEDAEEA003F7B8C /* UTMDownloadMacSupportToolsTask.swift in Sources */,
 				CE2D957024AD4F990059923A /* VMRemovableDrivesView.swift in Sources */,
 				CE25124B29BFE273000790AB /* UTMScriptable.swift in Sources */,
 				CE0B6CFE24AD56AE00FE012D /* UTMLogging.m in Sources */,

+ 0 - 114
patches/libsoup-2.74.2.patch

@@ -1,114 +0,0 @@
-From 119abc03aac8c5cf1af0845a0e64b3027ce1fa78 Mon Sep 17 00:00:00 2001
-From: osy <50960678+osy@users.noreply.github.com>
-Date: Sat, 5 Mar 2022 17:02:38 -0800
-Subject: [PATCH] soup-tld: disabled when libpsl is optional
-
-When building without libpsl, we no longer have soup-tld.c. As a result,
-we do not provide those APIs in the built library and additionally the
-following change is made to soup_cookie_jar_add_cookie_full()
-
-1. We no longer reject cookies for public domains
-2. If the accept policy is not SOUP_COOKIE_JAR_ACCEPT_ALWAYS we assume
-   all incoming cookie is third party and reject it.
----
- libsoup/meson.build       | 4 +++-
- libsoup/soup-cookie-jar.c | 6 ++++++
- meson.build               | 5 ++++-
- tests/meson.build         | 7 ++++++-
- 4 files changed, 19 insertions(+), 3 deletions(-)
-
-diff --git a/libsoup/meson.build b/libsoup/meson.build
-index e585b3fe..ec0aca23 100644
---- a/libsoup/meson.build
-+++ b/libsoup/meson.build
-@@ -76,7 +76,6 @@ soup_sources = [
-   'soup-socket.c',
-   'soup-socket-properties.c',
-   'soup-status.c',
--  'soup-tld.c',
-   'soup-uri.c',
-   'soup-value-utils.c',
-   'soup-version.c',
-@@ -208,6 +207,9 @@ if brotlidec_dep.found()
-   soup_headers += 'soup-brotli-decompressor.h'
- endif
- 
-+if libpsl_dep.found()
-+  soup_sources += 'soup-tld.c'
-+endif
- 
- includedir = join_paths(libsoup_api_name, meson.project_name())
- install_headers(soup_installed_headers, subdir : includedir)
-diff --git a/libsoup/soup-cookie-jar.c b/libsoup/soup-cookie-jar.c
-index c8231f0e..5e35e135 100644
---- a/libsoup/soup-cookie-jar.c
-+++ b/libsoup/soup-cookie-jar.c
-@@ -595,18 +595,24 @@ soup_cookie_jar_add_cookie_full (SoupCookieJar *jar, SoupCookie *cookie, SoupURI
- 	g_return_if_fail (SOUP_IS_COOKIE_JAR (jar));
- 	g_return_if_fail (cookie != NULL);
- 
-+#ifdef HAVE_TLD
- 	/* Never accept cookies for public domains. */
- 	if (!g_hostname_is_ip_address (cookie->domain) &&
- 	    soup_tld_domain_is_public_suffix (cookie->domain)) {
- 		soup_cookie_free (cookie);
- 		return;
- 	}
-+#endif
- 
- 	priv = soup_cookie_jar_get_instance_private (jar);
- 
-         if (first_party != NULL) {
-+#ifdef HAVE_TLD
-                 if (priv->accept_policy == SOUP_COOKIE_JAR_ACCEPT_NEVER ||
-                     incoming_cookie_is_third_party (jar, cookie, first_party, priv->accept_policy)) {
-+#else // no TLD, assume every cookie is third-party
-+                if (priv->accept_policy != SOUP_COOKIE_JAR_ACCEPT_ALWAYS) {
-+#endif
-                         soup_cookie_free (cookie);
-                         return;
-                 }
-diff --git a/meson.build b/meson.build
-index 3cc56fb9..5865dfc7 100644
---- a/meson.build
-+++ b/meson.build
-@@ -148,7 +148,10 @@ endif
- 
- libpsl_required_version = '>= 0.20'
- libpsl_dep = dependency('libpsl', version : libpsl_required_version,
--  fallback : ['libpsl', 'libpsl_dep'])
-+  fallback : ['libpsl', 'libpsl_dep'], required : false)
-+if libpsl_dep.found()
-+    cdata.set('HAVE_TLD', '1')
-+endif
- 
- if cc.has_function('gmtime_r', prefix : '#include <time.h>', args : default_source_flag)
-     cdata.set('HAVE_GMTIME_R', '1')
-diff --git a/tests/meson.build b/tests/meson.build
-index 5482aa86..d5b32a12 100644
---- a/tests/meson.build
-+++ b/tests/meson.build
-@@ -62,7 +62,6 @@ tests = [
-   ['ssl', true, []],
-   ['streaming', true, []],
-   ['timeout', true, []],
--  ['tld', true, []],
-   ['uri-parsing', true, []],
-   ['websocket', true, [libz_dep]]
- ]
-@@ -82,6 +81,12 @@ if brotlidec_dep.found()
-   endif
- endif
- 
-+if libpsl_dep.found()
-+  tests += [
-+    ['tld', true, []],
-+  ]
-+endif
-+
- if have_apache
-   tests += [
-     ['auth', false, []],
--- 
-2.32.0 (Apple Git-132)
-

+ 255 - 0
patches/libsoup-3.6.0.patch

@@ -0,0 +1,255 @@
+From 95102597efaddede487bd03c191fa0a08b70e3b6 Mon Sep 17 00:00:00 2001
+From: osy <osy@turing.llc>
+Date: Mon, 11 Nov 2024 14:47:39 -0800
+Subject: [PATCH 1/2] soup-tld: disabled when libpsl is optional
+
+When building without libpsl, we no longer have soup-tld.c. As a result,
+we do not provide those APIs in the built library and additionally the
+following change is made to soup_cookie_jar_add_cookie_full()
+
+1. We no longer reject cookies for public domains
+2. If the accept policy is not SOUP_COOKIE_JAR_ACCEPT_ALWAYS we assume
+   all incoming cookie is third party and reject it.
+---
+ libsoup/cookies/soup-cookie-jar.c | 15 +++++++++++++++
+ libsoup/meson.build               |  5 ++++-
+ meson.build                       |  5 ++++-
+ tests/meson.build                 |  5 ++++-
+ 4 files changed, 27 insertions(+), 3 deletions(-)
+
+diff --git a/libsoup/cookies/soup-cookie-jar.c b/libsoup/cookies/soup-cookie-jar.c
+index bdb6697a..753c36b5 100644
+--- a/libsoup/cookies/soup-cookie-jar.c
++++ b/libsoup/cookies/soup-cookie-jar.c
+@@ -511,6 +511,7 @@ normalize_cookie_domain (const char *domain)
+ 	return domain;
+ }
+ 
++#ifdef HAVE_TLD
+ static gboolean
+ incoming_cookie_is_third_party (SoupCookieJar            *jar,
+ 				SoupCookie               *cookie,
+@@ -563,6 +564,16 @@ incoming_cookie_is_third_party (SoupCookieJar            *jar,
+ 
+         return retval;
+ }
++#else
++static gboolean
++incoming_cookie_is_third_party (SoupCookieJar            *jar,
++				SoupCookie               *cookie,
++				GUri                     *first_party,
++				SoupCookieJarAcceptPolicy policy)
++{
++	return TRUE;
++}
++#endif
+ 
+ static gboolean
+ string_contains_ctrlcode (const char *s)
+@@ -612,7 +623,11 @@ soup_cookie_jar_add_cookie_full (SoupCookieJar *jar, SoupCookie *cookie, GUri *u
+ 
+ 	/* Never accept cookies for public domains. */
+ 	if (!g_hostname_is_ip_address (soup_cookie_get_domain (cookie)) &&
++#ifdef HAVE_TLD
+ 	    soup_tld_domain_is_public_suffix (soup_cookie_get_domain (cookie))) {
++#else
++	    priv->accept_policy != SOUP_COOKIE_JAR_ACCEPT_ALWAYS){
++#endif
+ 		soup_cookie_free (cookie);
+ 		return;
+ 	}
+diff --git a/libsoup/meson.build b/libsoup/meson.build
+index d920b522..b889931d 100644
+--- a/libsoup/meson.build
++++ b/libsoup/meson.build
+@@ -87,11 +87,14 @@ soup_sources = [
+   'soup-session-feature.c',
+   'soup-socket-properties.c',
+   'soup-status.c',
+-  'soup-tld.c',
+   'soup-uri-utils.c',
+   'soup-version.c',
+ ]
+ 
++if libpsl_dep.found()
++  soup_sources += 'soup-tld.c'
++endif
++
+ soup_private_enum_headers = [
+   'soup-connection.h',
+ ]
+diff --git a/meson.build b/meson.build
+index f7c63389..50ca7b91 100644
+--- a/meson.build
++++ b/meson.build
+@@ -155,7 +155,10 @@ endif
+ 
+ libpsl_required_version = '>= 0.20'
+ libpsl_dep = dependency('libpsl', version : libpsl_required_version,
+-  fallback : ['libpsl', 'libpsl_dep'])
++  fallback : ['libpsl', 'libpsl_dep'], required : false)
++if libnghttp2_dep.found()
++  cdata.set('HAVE_TLD', true)
++endif
+ 
+ if cc.has_function('gmtime_r', prefix : '#include <time.h>', args : default_source_flag)
+     cdata.set('HAVE_GMTIME_R', '1')
+diff --git a/tests/meson.build b/tests/meson.build
+index 01a0c63f..cf24ef97 100644
+--- a/tests/meson.build
++++ b/tests/meson.build
+@@ -102,12 +102,15 @@ tests = [
+   },
+   {'name': 'streaming'},
+   {'name': 'timeout'},
+-  {'name': 'tld'},
+   {'name': 'uri-parsing'},
+   {'name': 'websocket',
+    'dependencies': [libz_dep]},
+ ]
+ 
++if libpsl_dep.found()
++  tests += [{'name': 'tld'}]
++endif
++
+ if brotlidec_dep.found()
+   tests += [{'name': 'brotli-decompressor'}]
+ 
+-- 
+2.41.0
+
+From e4ce620a7db4d2f1a581a8095fea32a182b353aa Mon Sep 17 00:00:00 2001
+From: osy <osy@turing.llc>
+Date: Mon, 11 Nov 2024 14:48:15 -0800
+Subject: [PATCH 2/2] build: make HTTP2 optional
+
+---
+ libsoup/meson.build                     | 13 ++++++++-----
+ libsoup/server/soup-server-connection.c |  4 ++++
+ libsoup/soup-connection.c               |  4 ++++
+ meson.build                             |  9 ++++++---
+ tests/meson.build                       |  7 +++++--
+ 5 files changed, 27 insertions(+), 10 deletions(-)
+
+diff --git a/libsoup/meson.build b/libsoup/meson.build
+index b889931d..f2f4a0d7 100644
+--- a/libsoup/meson.build
++++ b/libsoup/meson.build
+@@ -39,11 +39,7 @@ soup_sources = [
+   'http1/soup-message-io-data.c',
+   'http1/soup-message-io-source.c',
+ 
+-  'http2/soup-client-message-io-http2.c',
+-  'http2/soup-body-input-stream-http2.c',
+-
+   'server/http1/soup-server-message-io-http1.c',
+-  'server/http2/soup-server-message-io-http2.c',
+   'server/soup-auth-domain.c',
+   'server/soup-auth-domain-basic.c',
+   'server/soup-auth-domain-digest.c',
+@@ -70,7 +66,6 @@ soup_sources = [
+   'soup-form.c',
+   'soup-headers.c',
+   'soup-header-names.c',
+-  'soup-http2-utils.c',
+   'soup-init.c',
+   'soup-io-stream.c',
+   'soup-logger.c',
+@@ -95,6 +90,14 @@ if libpsl_dep.found()
+   soup_sources += 'soup-tld.c'
+ endif
+ 
++if libnghttp2_dep.found()
++  soup_sources += 'http2/soup-client-message-io-http2.c'
++  soup_sources += 'http2/soup-body-input-stream-http2.c'
++  soup_sources += 'server/http2/soup-server-message-io-http2.c'
++  soup_sources += 'soup-http2-utils.c'
++endif
++
++
+ soup_private_enum_headers = [
+   'soup-connection.h',
+ ]
+diff --git a/libsoup/server/soup-server-connection.c b/libsoup/server/soup-server-connection.c
+index cac4eaa7..02fdb497 100644
+--- a/libsoup/server/soup-server-connection.c
++++ b/libsoup/server/soup-server-connection.c
+@@ -395,10 +395,14 @@ soup_server_connection_connected (SoupServerConnection *conn)
+                                                                   conn);
+                 break;
+         case SOUP_HTTP_2_0:
++#ifdef WITH_HTTP2
+                 priv->io_data = soup_server_message_io_http2_new (conn,
+                                                                   g_steal_pointer (&priv->initial_msg),
+                                                                   (SoupMessageIOStartedFn)request_started_cb,
+                                                                   conn);
++#else
++                g_assert_not_reached();
++#endif
+                 break;
+         }
+         g_signal_emit (conn, signals[CONNECTED], 0);
+diff --git a/libsoup/soup-connection.c b/libsoup/soup-connection.c
+index 9100f8c9..fc28cd22 100644
+--- a/libsoup/soup-connection.c
++++ b/libsoup/soup-connection.c
+@@ -504,7 +504,11 @@ soup_connection_create_io_data (SoupConnection *conn)
+                 priv->io_data = soup_client_message_io_http1_new (conn);
+                 break;
+         case SOUP_HTTP_2_0:
++#ifdef WITH_HTTP2
+                 priv->io_data = soup_client_message_io_http2_new (conn);
++#else
++                g_assert_not_reached();
++#endif
+                 break;
+         }
+ }
+diff --git a/meson.build b/meson.build
+index 50ca7b91..1ec35873 100644
+--- a/meson.build
++++ b/meson.build
+@@ -112,9 +112,12 @@ glib_deps = [glib_dep, gmodule_dep, gobject_dep, gio_dep]
+ 
+ cdata = configuration_data()
+ 
+-libnghttp2_dep = dependency('libnghttp2')
+-if (libnghttp2_dep.version() == 'unknown' and (libnghttp2_dep.type_name() == 'internal' or cc.has_function('nghttp2_option_set_no_rfc9113_leading_and_trailing_ws_validation', prefix : '#include <nghttp2/nghttp2.h>', dependencies : libnghttp2_dep))) or libnghttp2_dep.version().version_compare('>=1.50')
+-    cdata.set('HAVE_NGHTTP2_OPTION_SET_NO_RFC9113_LEADING_AND_TRAILING_WS_VALIDATION', '1')
++libnghttp2_dep = dependency('libnghttp2', required : false)
++if libnghttp2_dep.found()
++  cdata.set('WITH_HTTP2', true)
++  if (libnghttp2_dep.version() == 'unknown' and (libnghttp2_dep.type_name() == 'internal' or cc.has_function('nghttp2_option_set_no_rfc9113_leading_and_trailing_ws_validation', prefix : '#include <nghttp2/nghttp2.h>', dependencies : libnghttp2_dep))) or libnghttp2_dep.version().version_compare('>=1.50')
++      cdata.set('HAVE_NGHTTP2_OPTION_SET_NO_RFC9113_LEADING_AND_TRAILING_WS_VALIDATION', '1')
++  endif
+ endif
+ 
+ sqlite_dep = dependency('sqlite3', required: false)
+diff --git a/tests/meson.build b/tests/meson.build
+index cf24ef97..6bd68868 100644
+--- a/tests/meson.build
++++ b/tests/meson.build
+@@ -78,8 +78,6 @@ tests = [
+   {'name': 'date'},
+   {'name': 'forms'},
+   {'name': 'header-parsing'},
+-  {'name': 'http2'},
+-  {'name': 'http2-body-stream'},
+   {'name': 'hsts'},
+   {'name': 'hsts-db'},
+   {'name': 'logger'},
+@@ -111,6 +109,11 @@ if libpsl_dep.found()
+   tests += [{'name': 'tld'}]
+ endif
+ 
++if libnghttp2_dep.found()
++  tests += [{'name': 'http2'}]
++  tests += [{'name': 'http2-body-stream'}]
++endif
++
+ if brotlidec_dep.found()
+   tests += [{'name': 'brotli-decompressor'}]
+ 
+-- 
+2.41.0
+

+ 0 - 135
patches/phodav-2.5.patch

@@ -1,135 +0,0 @@
-From c0d495a77c7934e982d280a33deaaa9f6595785e Mon Sep 17 00:00:00 2001
-From: osy <50960678+osy@users.noreply.github.com>
-Date: Sat, 5 Mar 2022 17:40:07 -0800
-Subject: [PATCH 1/4] method: fix compile on Darwin
-
-On Darwin systems, removexattr() is defined with 3 arguments.
----
- libphodav/phodav-method-proppatch.c | 4 ++++
- tests/meson.build                   | 2 +-
- 2 files changed, 5 insertions(+), 1 deletion(-)
-
-diff --git a/libphodav/phodav-method-proppatch.c b/libphodav/phodav-method-proppatch.c
-index 4cd8211..3421e32 100644
---- a/libphodav/phodav-method-proppatch.c
-+++ b/libphodav/phodav-method-proppatch.c
-@@ -59,7 +59,11 @@ set_attr (GFile *file, xmlNodePtr attrnode,
-         return SOUP_STATUS_FORBIDDEN;
-       gchar *path = g_file_get_path (file);
- #ifdef HAVE_SYS_XATTR_H
-+#ifdef __APPLE__
-+      removexattr (path, attrname, 0);
-+#else
-       removexattr (path, attrname);
-+#endif
- #else
-       g_debug ("cannot remove xattr from %s, not supported", path); /* FIXME? */
- #endif
-diff --git a/tests/meson.build b/tests/meson.build
-index aeb48e3..43e9a13 100644
---- a/tests/meson.build
-+++ b/tests/meson.build
-@@ -1,6 +1,6 @@
- tests_sources = []
- 
--if host_machine.system() != 'windows'
-+if host_machine.system() not in ['darwin', 'ios', 'windows']
-   tests_sources += 'virtual-dir.c'
- endif
- 
--- 
-2.32.0 (Apple Git-132)
-
-From 8060e63fb82baba60dee6f3360780c6e83d16472 Mon Sep 17 00:00:00 2001
-From: osy <50960678+osy@users.noreply.github.com>
-Date: Sat, 5 Mar 2022 17:41:18 -0800
-Subject: [PATCH 2/4] meson: fix build on unsupported --no-undefined
-
-Clang on Darwin systems do not support this flag.
----
- libphodav/meson.build | 7 +++++--
- 1 file changed, 5 insertions(+), 2 deletions(-)
-
-diff --git a/libphodav/meson.build b/libphodav/meson.build
-index 5443ce0..4ab6821 100644
---- a/libphodav/meson.build
-+++ b/libphodav/meson.build
-@@ -30,7 +30,10 @@ if not dependency('glib-2.0', version : '>= 2.51.2', required: false).found()
- endif
- 
- mapfile = 'libphodav.syms'
--vflag = '-Wl,--version-script,@0@/@1@'.format(meson.current_source_dir(), mapfile)
-+vflag = compiler.get_supported_link_arguments(
-+  '-Wl,--no-undefined',
-+  '-Wl,--version-script,@0@/@1@'.format(meson.current_source_dir(), mapfile)
-+)
- 
- libphodav = library(
-   'phodav-2.0',
-@@ -38,7 +41,7 @@ libphodav = library(
-   c_args : [ '-DG_LOG_DOMAIN="phodav"' ],
-   include_directories : incdir,
-   version: '0.0.0',
--  link_args : [ '-Wl,--no-undefined', vflag ],
-+  link_args : vflag,
-   link_depends : mapfile,
-   dependencies : deps,
-   install : true,
--- 
-2.32.0 (Apple Git-132)
-
-From 450361cefca48f6b8ca191a7024cad29beaa0825 Mon Sep 17 00:00:00 2001
-From: osy <50960678+osy@users.noreply.github.com>
-Date: Sat, 5 Mar 2022 17:49:26 -0800
-Subject: [PATCH 3/4] spice-webdavd: support macOS port
-
----
- bin/spice-webdavd.c | 4 ++++
- 1 file changed, 4 insertions(+)
-
-diff --git a/bin/spice-webdavd.c b/bin/spice-webdavd.c
-index ee713bd..b9453ff 100644
---- a/bin/spice-webdavd.c
-+++ b/bin/spice-webdavd.c
-@@ -655,7 +655,11 @@ run_service (ServiceData *service_data)
- 
-   loop = g_main_loop_new (NULL, TRUE);
- #ifdef G_OS_UNIX
-+#ifdef __APPLE__
-+  open_mux_path ("/dev/tty.org.spice-space.webdav.0");
-+#else
-   open_mux_path ("/dev/virtio-ports/org.spice-space.webdav.0");
-+#endif
- #else
-   open_mux_path ("\\\\.\\Global\\org.spice-space.webdav.0");
- #endif
--- 
-2.32.0 (Apple Git-132)
-
-From f5c7f192644d8f30817ab23a98425e3179a0021d Mon Sep 17 00:00:00 2001
-From: osy <50960678+osy@users.noreply.github.com>
-Date: Sat, 5 Mar 2022 23:40:27 -0800
-Subject: [PATCH 4/4] meson: link statically with libsoup and libxml
-
----
- meson.build | 4 ++--
- 1 file changed, 2 insertions(+), 2 deletions(-)
-
-diff --git a/meson.build b/meson.build
-index b8ff125..7ab6da1 100644
---- a/meson.build
-+++ b/meson.build
-@@ -34,8 +34,8 @@ else
-   deps += dependency('gio-unix-2.0', version : '>= 2.44')
- endif
- 
--deps += dependency('libsoup-2.4', version : '>= 2.48.0')
--deps += dependency('libxml-2.0')
-+deps += dependency('libsoup-2.4', version : '>= 2.48.0', static : true)
-+deps += dependency('libxml-2.0', static : true)
- 
- d1 = dependency('avahi-gobject', required : get_option('avahi'))
- d2 = dependency('avahi-client', required : get_option('avahi'))
--- 
-2.32.0 (Apple Git-132)
-

+ 27 - 0
patches/phodav-3.0.patch

@@ -0,0 +1,27 @@
+From ddca2a3c7a5cabf19ae94e4a6482457cb5fa1b30 Mon Sep 17 00:00:00 2001
+From: osy <osy@turing.llc>
+Date: Tue, 12 Nov 2024 08:51:07 -0800
+Subject: [PATCH] meson: link statically with libsoup and libxml
+
+---
+ meson.build | 4 ++--
+ 1 file changed, 2 insertions(+), 2 deletions(-)
+
+diff --git a/meson.build b/meson.build
+index ac84b94..c425839 100644
+--- a/meson.build
++++ b/meson.build
+@@ -34,8 +34,8 @@ else
+   deps += dependency('gio-unix-2.0', version : '>= 2.44')
+ endif
+ 
+-deps += dependency('libsoup-3.0', version : '>= 3.0.0')
+-deps += dependency('libxml-2.0')
++deps += dependency('libsoup-3.0', version : '>= 3.0.0', static : true)
++deps += dependency('libxml-2.0', static : true)
+ 
+ d1 = dependency('avahi-gobject', required : get_option('avahi'))
+ d2 = dependency('avahi-client', required : get_option('avahi'))
+-- 
+2.41.0
+

+ 28 - 0
patches/qemu-9.1.0-utm.patch

@@ -126,3 +126,31 @@ index 4c2dd33532..6e73c6e13e 100644
 -- 
 2.41.0
 
+From bf72f711841fc8d308015a5768f70563669ff766 Mon Sep 17 00:00:00 2001
+From: osy <osy@turing.llc>
+Date: Mon, 11 Nov 2024 01:45:20 -0800
+Subject: [PATCH] hvf: arm: disable SME which is not properly handled by QEMU
+
+---
+ target/arm/hvf/hvf.c | 5 +++++
+ 1 file changed, 5 insertions(+)
+
+diff --git a/target/arm/hvf/hvf.c b/target/arm/hvf/hvf.c
+index 5411af348b..e95e12c9c1 100644
+--- a/target/arm/hvf/hvf.c
++++ b/target/arm/hvf/hvf.c
+@@ -889,6 +889,11 @@ static bool hvf_arm_get_host_cpu_features(ARMHostCPUFeatures *ahcf)
+     r |= hv_vcpu_get_sys_reg(fd, HV_SYS_REG_MIDR_EL1, &ahcf->midr);
+     r |= hv_vcpu_destroy(fd);
+ 
++    /*
++     * Disable SME which is not properly handled by QEMU yet
++     */
++    host_isar.id_aa64pfr1 &= ~R_ID_AA64PFR1_SME_MASK;
++
+     ahcf->isar = host_isar;
+ 
+     /*
+-- 
+2.41.0
+

+ 6 - 6
patches/sources

@@ -9,7 +9,7 @@ ICONV_SRC="https://ftp.gnu.org/gnu/libiconv/libiconv-1.16.tar.gz"
 GETTEXT_SRC="https://ftp.gnu.org/gnu/gettext/gettext-0.22.5.tar.gz"
 PNG_SRC="https://ftp.osuosl.org/pub/blfs/conglomeration/libpng/libpng-1.6.37.tar.xz"
 JPEG_TURBO_SRC="https://ftp.osuosl.org/pub/blfs/conglomeration/libjpeg-turbo/libjpeg-turbo-1.5.3.tar.gz"
-GLIB_SRC="https://download.gnome.org/sources/glib/2.69/glib-2.69.0.tar.xz"
+GLIB_SRC="https://download.gnome.org/sources/glib/2.83/glib-2.83.0.tar.xz"
 GPG_ERROR_SRC="https://www.gnupg.org/ftp/gcrypt/libgpg-error/libgpg-error-1.38.tar.gz"
 GCRYPT_SRC="https://www.gnupg.org/ftp/gcrypt/libgcrypt/libgcrypt-1.8.4.tar.gz"
 PIXMAN_SRC="https://www.cairographics.org/releases/pixman-0.38.0.tar.gz"
@@ -21,19 +21,19 @@ ZSTD_SRC="https://github.com/facebook/zstd/releases/download/v1.5.2/zstd-1.5.2.t
 SPICE_PROTOCOL_SRC="https://www.spice-space.org/download/releases/spice-protocol-0.14.4.tar.xz"
 SPICE_SERVER_SRC="https://www.spice-space.org/download/releases/spice-server/spice-0.14.3.tar.bz2"
 USB_SRC="https://github.com/libusb/libusb/releases/download/v1.0.25/libusb-1.0.25.tar.bz2"
-USBREDIR_SRC="https://www.spice-space.org/download/usbredir/usbredir-0.13.0.tar.xz"
+USBREDIR_SRC="https://www.spice-space.org/download/usbredir/usbredir-0.14.0.tar.xz"
 SLIRP_SRC="https://gitlab.freedesktop.org/slirp/libslirp/-/archive/v4.7.0/libslirp-v4.7.0.tar.gz"
 QEMU_SRC="https://github.com/utmapp/qemu/releases/download/v9.1.0-utm/qemu-9.1.0-utm.tar.xz"
 
 # Source files for spice-client
-JSON_GLIB_SRC="https://download.gnome.org/sources/json-glib/1.6/json-glib-1.6.6.tar.xz"
+JSON_GLIB_SRC="https://download.gnome.org/sources/json-glib/1.10/json-glib-1.10.0.tar.xz"
 GST_SRC="https://gstreamer.freedesktop.org/src/gstreamer/gstreamer-1.19.1.tar.xz"
 GST_BASE_SRC="https://gstreamer.freedesktop.org/src/gst-plugins-base/gst-plugins-base-1.19.1.tar.xz"
 GST_GOOD_SRC="https://gstreamer.freedesktop.org/src/gst-plugins-good/gst-plugins-good-1.19.1.tar.xz"
 XML2_SRC="http://xmlsoft.org/sources/libxml2-2.9.12.tar.gz"
-SOUP_SRC="https://download.gnome.org/sources/libsoup/2.74/libsoup-2.74.2.tar.xz"
-PHODAV_SRC="https://download.gnome.org/sources/phodav/2.5/phodav-2.5.tar.xz"
-SPICE_CLIENT_SRC="https://www.spice-space.org/download/gtk/spice-gtk-0.40.tar.xz"
+SOUP_SRC="https://download.gnome.org/sources/libsoup/3.6/libsoup-3.6.0.tar.xz"
+PHODAV_SRC="https://download.gnome.org/sources/phodav/3.0/phodav-3.0.tar.xz"
+SPICE_CLIENT_SRC="https://www.spice-space.org/download/gtk/spice-gtk-0.42.tar.xz"
 LIBUCONTEXT_REPO="https://github.com/utmapp/libucontext.git"
 LIBUCONTEXT_COMMIT="9b1d8f01a6e99166f9808c79966abe10786de8b6"
 

+ 14 - 548
patches/spice-gtk-0.40.patch → patches/spice-gtk-0.42.patch

@@ -1,134 +1,7 @@
-From 2f16f6d4b0d6dde0d1d518f61c01f5f972caa008 Mon Sep 17 00:00:00 2001
-From: osy <osy@turing.llc>
-Date: Fri, 4 Mar 2022 13:15:16 -0800
-Subject: [PATCH 1/8] meson: move cairo dependency to GTK build only
-
-Cairo is only used in SpiceDisplay which is part of the GTK client. If
-we are building the GLib only client, it should be optional.
----
- meson.build | 6 ++++--
- 1 file changed, 4 insertions(+), 2 deletions(-)
-
-diff --git a/meson.build b/meson.build
-index 11173fd..ecc9d6d 100644
---- a/meson.build
-+++ b/meson.build
-@@ -107,8 +107,8 @@ foreach dep, version : deps
- endforeach
- 
- # mandatory dependencies, without specific version requirement
--# TODO: specify minimum version for cairo, jpeg and zlib?
--deps = ['cairo', 'libjpeg', 'zlib', 'json-glib-1.0']
-+# TODO: specify minimum version for jpeg and zlib?
-+deps = ['libjpeg', 'zlib', 'json-glib-1.0']
- if host_machine.system() == 'windows'
-   deps += 'gio-windows-2.0'
- else
-@@ -149,6 +149,8 @@ d = dependency('gtk+-3.0', version : '>= @0@'.format(gtk_version_required),
- summary_info += {'gtk': d.found()}
- if d.found()
-   spice_gtk_deps += d
-+  # TODO: specify minimum version for cairo?
-+  spice_gtk_deps += dependency('cairo')
-   if host_machine.system() != 'windows'
-     spice_gtk_deps += dependency('epoxy')
-     spice_gtk_deps += dependency('x11')
--- 
-2.32.0 (Apple Git-132)
-
-From 312a1fc6cf4a8d839639dce411107537e1791045 Mon Sep 17 00:00:00 2001
-From: osy <osy@turing.llc>
-Date: Fri, 4 Mar 2022 15:52:48 -0800
-Subject: [PATCH 2/8] coroutine: add support for libucontext
-
-libucontext is a lightweight implementation of ucontext for platforms
-that do not have a built-in implementation. This allows us to use the
-same code to support libucontext as ucontext.
----
- meson.build        |  7 +++++++
- meson_options.txt  |  2 +-
- src/continuation.c | 12 +++++++++++-
- src/meson.build    |  2 +-
- 4 files changed, 20 insertions(+), 3 deletions(-)
-
-diff --git a/meson.build b/meson.build
-index ecc9d6d..29615b1 100644
---- a/meson.build
-+++ b/meson.build
-@@ -319,6 +319,13 @@ if spice_gtk_coroutine == 'ucontext'
-   endif
-   endif
- 
-+if spice_gtk_coroutine == 'libucontext'
-+  d = dependency('libucontext')
-+  spice_glib_deps += d
-+  spice_gtk_config_data.set('WITH_UCONTEXT', '1')
-+  spice_gtk_config_data.set('HAVE_LIBUCONTEXT', '1')
-+endif
-+
- if spice_gtk_coroutine == 'gthread'
-   spice_gtk_config_data.set('WITH_GTHREAD', '1')
- endif
-diff --git a/meson_options.txt b/meson_options.txt
-index 3cbc7c6..5acfc9a 100644
---- a/meson_options.txt
-+++ b/meson_options.txt
-@@ -45,7 +45,7 @@ option('usb-ids-path',
- option('coroutine',
-     type : 'combo',
-     value : 'auto',
--    choices : ['auto', 'ucontext', 'gthread', 'winfiber'],
-+    choices : ['auto', 'ucontext', 'libucontext', 'gthread', 'winfiber'],
-     description : 'Use ucontext or GThread for coroutines')
- 
- option('introspection',
-diff --git a/src/continuation.c b/src/continuation.c
-index 65527ac..400169a 100644
---- a/src/continuation.c
-+++ b/src/continuation.c
-@@ -25,11 +25,21 @@
- #endif
- 
- #include <errno.h>
--#include <ucontext.h>
- #include <glib.h>
- 
- #include "continuation.h"
- 
-+#ifdef HAVE_LIBUCONTEXT
-+#include <libucontext/libucontext.h>
-+#define ucontext_t libucontext_ucontext_t
-+#define getcontext libucontext_getcontext
-+#define setcontext libucontext_setcontext
-+#define swapcontext libucontext_swapcontext
-+#define makecontext libucontext_makecontext
-+#else
-+#include <ucontext.h>
-+#endif
-+
- /*
-  * va_args to makecontext() must be type 'int', so passing
-  * the pointer we need may require several int args. This
-diff --git a/src/meson.build b/src/meson.build
-index a9dfc57..961779f 100644
---- a/src/meson.build
-+++ b/src/meson.build
-@@ -146,7 +146,7 @@ endif
- 
- if spice_gtk_coroutine == 'gthread'
-   spice_client_glib_sources += 'coroutine_gthread.c'
--elif spice_gtk_coroutine == 'ucontext'
-+elif spice_gtk_coroutine in ['ucontext', 'libucontext']
-   spice_client_glib_sources += ['continuation.c',
-                                 'continuation.h',
-                                 'coroutine_ucontext.c']
--- 
-2.32.0 (Apple Git-132)
-
-From fb47817a4963a6e64d76bccb562cf5dbe2f628c1 Mon Sep 17 00:00:00 2001
+From 07ba2d801b4a03125dee3f9d5f4a13cad8d62008 Mon Sep 17 00:00:00 2001
 From: osy <osy@turing.llc>
 Date: Fri, 4 Mar 2022 16:35:26 -0800
-Subject: [PATCH 3/8] spice-util: support for non-default GMainContext
+Subject: [PATCH 1/2] spice-util: support for non-default GMainContext
 
 When spice-gtk is used in an application with its own GMainContext, the
 wrong context will be used leading to various issues.
@@ -387,12 +260,12 @@ index 421b4b0..e161c83 100644
  #define SPICE_DEBUG(fmt, ...)                                   \
      do {                                                        \
 -- 
-2.32.0 (Apple Git-132)
+2.41.0
 
-From a02df4084ff43c5796f1ead29ab9d67da48dff1e Mon Sep 17 00:00:00 2001
+From 92ac46d9328afa036e2e3aebf0f7218ba5b2910f Mon Sep 17 00:00:00 2001
 From: osy <osy@turing.llc>
 Date: Fri, 4 Mar 2022 16:44:20 -0800
-Subject: [PATCH 4/8] spice-gtk: user specified GMainContext for events
+Subject: [PATCH 2/2] spice-gtk: user specified GMainContext for events
 
 Following the previous commit, this replaces all GLib calls that
 implicitly uses the default main context with versions that can use the
@@ -747,7 +620,7 @@ index bb97ad7..8cc2dd1 100644
  
      return id;
 diff --git a/src/spice-channel.c b/src/spice-channel.c
-index d6199a5..d5070a9 100644
+index 3fd42c5..813923a 100644
 --- a/src/spice-channel.c
 +++ b/src/spice-channel.c
 @@ -744,9 +744,9 @@ void spice_msg_out_send(SpiceMsgOut *out)
@@ -763,7 +636,7 @@ index d6199a5..d5070a9 100644
      }
  
  end:
-@@ -2703,7 +2703,7 @@ cleanup:
+@@ -2748,7 +2748,7 @@ cleanup:
          c->event = SPICE_CHANNEL_ERROR_CONNECT;
      }
  
@@ -772,7 +645,7 @@ index d6199a5..d5070a9 100644
      /* Co-routine exits now - the SpiceChannel object may no longer exist,
         so don't do anything else now unless you like SEGVs */
      return NULL;
-@@ -2762,7 +2762,7 @@ static gboolean channel_connect(SpiceChannel *channel, gboolean tls)
+@@ -2807,7 +2807,7 @@ static gboolean channel_connect(SpiceChannel *channel, gboolean tls)
      g_object_ref(G_OBJECT(channel)); /* Unref'd when co-routine exits */
  
      /* we connect in idle, to let previous coroutine exit, if present */
@@ -781,7 +654,7 @@ index d6199a5..d5070a9 100644
  
      return true;
  }
-@@ -2828,7 +2828,7 @@ static void channel_reset(SpiceChannel *channel, gboolean migrating)
+@@ -2873,7 +2873,7 @@ static void channel_reset(SpiceChannel *channel, gboolean migrating)
  
      CHANNEL_DEBUG(channel, "channel reset");
      if (c->connect_delayed_id) {
@@ -790,7 +663,7 @@ index d6199a5..d5070a9 100644
          c->connect_delayed_id = 0;
      }
  
-@@ -2860,7 +2860,7 @@ static void channel_reset(SpiceChannel *channel, gboolean migrating)
+@@ -2905,7 +2905,7 @@ static void channel_reset(SpiceChannel *channel, gboolean migrating)
      g_queue_foreach(&c->xmit_queue, (GFunc)spice_msg_out_unref, NULL);
      g_queue_clear(&c->xmit_queue);
      if (c->xmit_queue_wakeup_id) {
@@ -922,7 +795,7 @@ index bb3c6cd..9d161ee 100644
      coroutine_yield(NULL);
  
 diff --git a/src/spice-widget.c b/src/spice-widget.c
-index 5f7c061..3e3fe05 100644
+index 6311115..19dff68 100644
 --- a/src/spice-widget.c
 +++ b/src/spice-widget.c
 @@ -55,6 +55,7 @@
@@ -1025,416 +898,9 @@ index e26b939..6054f3e 100644
          g_clear_object(&self->task);
      }
 -- 
-2.32.0 (Apple Git-132)
-
-From d24779edda0a889937131818b13e4f57a68a8169 Mon Sep 17 00:00:00 2001
-From: osy <osy@turing.llc>
-Date: Fri, 4 Mar 2022 21:23:51 -0800
-Subject: [PATCH 5/8] usb-device-cd: option to disable physical CD
-
-On iOS, there is no "sys/disk.h" header and cannot build the CD
-emulation code. This should not prevent the rest of USB redirection from
-working.
----
- meson.build         |  8 ++++++++
- meson_options.txt   |  4 ++++
- src/usb-device-cd.c | 36 +++++++++++++++++++++++++++++++++++-
- 3 files changed, 47 insertions(+), 1 deletion(-)
-
-diff --git a/meson.build b/meson.build
-index 29615b1..8c06666 100644
---- a/meson.build
-+++ b/meson.build
-@@ -228,11 +228,19 @@ if d1.found() and d2.found() and d3.found()
-     spice_glib_deps += [d1, d2, d3]
-     spice_gtk_config_data.set('USE_USBREDIR', '1')
-     spice_gtk_has_usbredir = true
-+    if get_option('physical-cd').allowed()
-+      spice_gtk_config_data.set('HAVE_PHYSICAL_CD', '1')
-+    endif
-   else
-     warning('USB redirection disabled on big endian machine as ' +
-             'usbredir only support little endian')
-   endif
- endif
-+summary_info += {'physical-cd': get_option('physical-cd')}
-+
-+if get_option('physical-cd').enabled() and not spice_gtk_has_usbredir
-+  error('Physical CD support cannot be enabled without USB redirection support!')
-+endif
- 
- d = dependency('libcap-ng', required : get_option('libcap-ng'))
- summary_info += {'libcap-ng': d.found()}
-diff --git a/meson_options.txt b/meson_options.txt
-index 5acfc9a..557ef6a 100644
---- a/meson_options.txt
-+++ b/meson_options.txt
-@@ -19,6 +19,10 @@ option('usbredir',
-     type : 'feature',
-     description : 'Enable usbredir support')
- 
-+option('physical-cd',
-+    type : 'feature',
-+    description : 'Enable support of physical CD drives')
-+
- option('libcap-ng',
-        type : 'feature',
-        description: 'Enable libcap-ng support for the USB acl helper')
-diff --git a/src/usb-device-cd.c b/src/usb-device-cd.c
-index 2bfeb3a..41d2e13 100644
---- a/src/usb-device-cd.c
-+++ b/src/usb-device-cd.c
-@@ -32,11 +32,14 @@
- 
- #ifdef G_OS_WIN32
- #include <windows.h>
-+#ifdef HAVE_PHYSICAL_CD
- #include <ntddcdrm.h>
- #include <ntddmmc.h>
-+#endif // HAVE_PHYSICAL_CD
- #else
- #include <sys/stat.h>
- #include <sys/ioctl.h>
-+#ifdef HAVE_PHYSICAL_CD
- #ifdef __APPLE__
- #include <sys/disk.h>
- #include <fcntl.h>
-@@ -44,6 +47,7 @@
- #include <linux/fs.h>
- #include <linux/cdrom.h>
- #endif
-+#endif // HAVE_PHYSICAL_CD
- #endif
- 
- #include "usb-emulation.h"
-@@ -120,6 +124,7 @@ static int cd_device_open_stream(SpiceCdLU *unit, const char *filename)
-     }
- 
-     struct stat file_stat = { 0 };
-+#ifdef HAVE_PHYSICAL_CD
-     if (fstat(fd, &file_stat) || file_stat.st_size == 0) {
-         file_stat.st_size = 0;
-         unit->device = 1;
-@@ -138,6 +143,12 @@ static int cd_device_open_stream(SpiceCdLU *unit, const char *filename)
-         }
- #endif
-     }
-+#else // HAVE_PHYSICAL_CD
-+    if (fstat(fd, &file_stat) != 0) {
-+        SPICE_DEBUG("%s: can't run stat on %s", __FUNCTION__, unit->filename);
-+        return -1;
-+    }
-+#endif
-     unit->size = file_stat.st_size;
-     close(fd);
-     if (unit->size) {
-@@ -153,6 +164,8 @@ static int cd_device_open_stream(SpiceCdLU *unit, const char *filename)
-     return 0;
- }
- 
-+#if defined HAVE_PHYSICAL_CD
-+
- static int cd_device_load(SpiceCdLU *unit, gboolean load)
- {
-     int error;
-@@ -214,7 +227,11 @@ static int cd_device_check(SpiceCdLU *unit)
-     return error;
- }
- 
--#else
-+#endif // HAVE_PHYSICAL_CD
-+
-+#else // G_OS_WIN32
-+
-+#ifdef HAVE_PHYSICAL_CD
- 
- static gboolean is_device_name(const char *filename)
- {
-@@ -261,6 +278,8 @@ static gboolean check_device(HANDLE h)
-                            &ret, NULL);
- }
- 
-+#endif // HAVE_PHYSICAL_CD
-+
- static int cd_device_open_stream(SpiceCdLU *unit, const char *filename)
- {
-     HANDLE h;
-@@ -275,8 +294,10 @@ static int cd_device_open_stream(SpiceCdLU *unit, const char *filename)
-     }
-     if (!filename) {
-         // reopening the stream on existing file name
-+#if defined HAVE_PHYSICAL_CD
-     } else if (is_device_name(filename)) {
-         unit->filename = g_strdup_printf("\\\\.\\%s", filename);
-+#endif
-     } else {
-         unit->filename = g_strdup(filename);
-     }
-@@ -287,6 +308,7 @@ static int cd_device_open_stream(SpiceCdLU *unit, const char *filename)
-     }
- 
-     LARGE_INTEGER size = { 0 };
-+#if defined HAVE_PHYSICAL_CD
-     if (!GetFileSizeEx(h, &size)) {
-         uint64_t buffer[256];
-         uint32_t res;
-@@ -304,6 +326,12 @@ static int cd_device_open_stream(SpiceCdLU *unit, const char *filename)
-                 __FUNCTION__, unit->filename, res);
-         }
-     }
-+#else
-+    if (!GetFileSizeEx(h, &size)) {
-+        SPICE_DEBUG("%s: can't get file size for %s", __FUNCTION__, unit->filename);
-+        return -1;
-+    }
-+#endif
-     unit->size = size.QuadPart;
-     CloseHandle(h);
-     if (unit->size) {
-@@ -318,6 +346,8 @@ static int cd_device_open_stream(SpiceCdLU *unit, const char *filename)
-     return 0;
- }
- 
-+#ifdef HAVE_PHYSICAL_CD
-+
- static int cd_device_load(SpiceCdLU *unit, gboolean load)
- {
-     int error = 0;
-@@ -363,6 +393,8 @@ static int cd_device_check(SpiceCdLU *unit)
-     return error;
- }
- 
-+#endif // HAVE_PHYSICAL_CD
-+
- #endif
- 
- static gboolean open_stream(SpiceCdLU *unit, const char *filename)
-@@ -380,6 +412,7 @@ static void close_stream(SpiceCdLU *unit)
- static gboolean load_lun(UsbCd *d, int unit, gboolean load)
- {
-     gboolean b = TRUE;
-+#ifdef HAVE_PHYSICAL_CD
-     if (load && d->units[unit].device) {
-         // there is one possible problem in case our backend is the
-         // local CD device and it is ejected
-@@ -389,6 +422,7 @@ static gboolean load_lun(UsbCd *d, int unit, gboolean load)
-             return FALSE;
-         }
-     }
-+#endif
- 
-     if (load) {
-         CdScsiMediaParameters media_params = { 0 };
--- 
-2.32.0 (Apple Git-132)
-
-From 7b572d38a2d4a32ecdd683cc4672abd00dcc07ff Mon Sep 17 00:00:00 2001
-From: osy <osy@turing.llc>
-Date: Sun, 6 Mar 2022 18:49:34 -0800
-Subject: [PATCH 6/8] gitlab-ci: test disable physical cd
-
----
- .gitlab-ci.yml | 4 ++--
- 1 file changed, 2 insertions(+), 2 deletions(-)
-
-diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
-index 3d4f533..cdd6575 100644
---- a/.gitlab-ci.yml
-+++ b/.gitlab-ci.yml
-@@ -36,7 +36,7 @@ fedora:
-     - ninja -C build-spice-protocol install
- 
-   script:
--    - meson --buildtype=release build-default --werror
-+    - meson --buildtype=release build-default --werror -Dphysical-cd=disabled
-     # Meson does not update submodules recursively
-     - git submodule update --init --recursive
-     # this fix an issue with Meson dist
-@@ -68,6 +68,6 @@ windows:
-   script:
-     - cd $CI_PROJECT_DIR
-     - mkdir build-win64 && cd build-win64
--    - mingw64-meson --buildtype=release -Dgtk_doc=disabled --werror
-+    - mingw64-meson --buildtype=release -Dgtk_doc=disabled --werror -Dphysical-cd=disabled
-     - ninja install
-     - (cd tests && DISPLAY= WINEPATH=/usr/x86_64-w64-mingw32/sys-root/mingw/bin wine test-coroutine.exe)
--- 
-2.32.0 (Apple Git-132)
-
-From 6069f4abaf26dadb2159ec67e7b362e2485d3652 Mon Sep 17 00:00:00 2001
-From: osy <osy@turing.llc>
-Date: Sun, 6 Mar 2022 18:55:14 -0800
-Subject: [PATCH 7/8] fix windows
-
----
- src/usb-device-cd.c | 6 ++----
- 1 file changed, 2 insertions(+), 4 deletions(-)
-
-diff --git a/src/usb-device-cd.c b/src/usb-device-cd.c
-index 41d2e13..d0cac30 100644
---- a/src/usb-device-cd.c
-+++ b/src/usb-device-cd.c
-@@ -231,8 +231,6 @@ static int cd_device_check(SpiceCdLU *unit)
- 
- #else // G_OS_WIN32
- 
--#ifdef HAVE_PHYSICAL_CD
--
- static gboolean is_device_name(const char *filename)
- {
-     return g_ascii_isalpha(filename[0]) && filename[1] == ':' &&
-@@ -253,6 +251,8 @@ static HANDLE open_file(const char *filename)
-     return h;
- }
- 
-+#ifdef HAVE_PHYSICAL_CD
-+
- static uint32_t ioctl_out(HANDLE h, uint32_t code, void *out_buffer, uint32_t out_size)
- {
-     uint32_t error;
-@@ -294,10 +294,8 @@ static int cd_device_open_stream(SpiceCdLU *unit, const char *filename)
-     }
-     if (!filename) {
-         // reopening the stream on existing file name
--#if defined HAVE_PHYSICAL_CD
-     } else if (is_device_name(filename)) {
-         unit->filename = g_strdup_printf("\\\\.\\%s", filename);
--#endif
-     } else {
-         unit->filename = g_strdup(filename);
-     }
--- 
-2.32.0 (Apple Git-132)
-
-From 394ec4a8d5f1c4df2c21c335a64627ebe31e03b1 Mon Sep 17 00:00:00 2001
-From: osy <osy@turing.llc>
-Date: Fri, 20 May 2022 08:53:53 -0700
-Subject: [PATCH 8/8] usb-backend: remove incorrect logic for detecting root
- hub
-
-There are valid devices (on Darwin) with address 0x1 that were ignored.
----
- src/usb-backend.c | 4 +---
- 1 file changed, 1 insertion(+), 3 deletions(-)
-
-diff --git a/src/usb-backend.c b/src/usb-backend.c
-index 930ae4e..7c2df7f 100644
---- a/src/usb-backend.c
-+++ b/src/usb-backend.c
-@@ -121,9 +121,7 @@ static gboolean fill_usb_info(SpiceUsbDevice *dev)
-     UsbDeviceInformation *info = &dev->device_info;
-     get_usb_device_info_from_libusb_device(info, dev->libusb_device);
- 
--    if (info->address == 0xff || /* root hub (HCD) */
--        info->address <= 1 || /* root hub or bad address */
--        (info->class == LIBUSB_CLASS_HUB) /*hub*/) {
-+    if (info->class == LIBUSB_CLASS_HUB) /*hub*/ {
-         return FALSE;
-     }
-     return TRUE;
--- 
-2.32.0 (Apple Git-132)
-
-From b3eb04485cf4553b0e588a7ca78f7377e1c4f35e Mon Sep 17 00:00:00 2001
-From: Eli Schwartz <eschwartz93@gmail.com>
-Date: Mon, 27 Jun 2022 01:48:02 -0400
-Subject: [PATCH] fix invalid use of subprojects
-MIME-Version: 1.0
-Content-Type: text/plain; charset=UTF-8
-Content-Transfer-Encoding: 8bit
-
-The keycodemapdb Meson subproject provides a program and a source input.
-Since it is a subproject, Meson wants to sandbox that and requires it to
-be explicitly exported. But this never happened -- instead, we manually
-poked at files using the actual string path "subprojects/......"
-
-This was always a Meson sandbox violation, but Meson 0.63.0 started
-noticing it and erroring out.
-
-Instead, do the right thing. Update the subproject to a version that has
-a meson.build with actually meaningful contents -- namely, a files
-variable and a found program. Then use these in order to run the needed
-custom_target.
-
-In the process, it is also necessary to correct the argument ordering
-when running keymap-gen.
-
-Reviewed-by: Marc-André Lureau <marcandre.lureau@redhat.com>
----
- meson.build              | 7 ++++---
- src/meson.build          | 2 +-
- subprojects/keycodemapdb | 2 +-
- 3 files changed, 6 insertions(+), 5 deletions(-)
-
-diff --git a/meson.build b/meson.build
-index dc7b4272..00aff30e 100644
---- a/meson.build
-+++ b/meson.build
-@@ -49,9 +49,10 @@ spice_gtk_config_data.merge_from(spice_common.get_variable('spice_common_config_
- spice_glib_deps += spice_common.get_variable('spice_common_client_dep')
- spice_protocol_version = spice_common.get_variable('spice_protocol_version')
- 
--subproject('keycodemapdb')
--keymapgen = files('subprojects/keycodemapdb/tools/keymap-gen')
--keymapcsv = files('subprojects/keycodemapdb/data/keymaps.csv')
-+keycodemapdb = subproject('keycodemapdb')
-+
-+keymapgen = find_program('keymap-gen')
-+keymapcsv = keycodemapdb.get_variable('keymaps_csv')
- 
- #
- # check for system headers
-diff --git a/src/meson.build b/src/meson.build
-index 961779fc..32574e8e 100644
---- a/src/meson.build
-+++ b/src/meson.build
-@@ -312,7 +312,7 @@ if spice_gtk_has_gtk
-   foreach keymap : keymaps
-     varname = 'keymap_@0@2xtkbd'.format(keymap)
-     target = 'vncdisplay@0@.h'.format(varname)
--    cmd = [python, keymapgen, '--lang', 'glib2', '--varname', varname, 'code-map', keymapcsv, keymap, 'xtkbd']
-+    cmd = [python, keymapgen, 'code-map', '--lang', 'glib2', '--varname', varname, keymapcsv, keymap, 'xtkbd']
-     spice_client_gtk_sources += custom_target(target,
-                                               output : target,
-                                               capture : true,
--- 
-GitLab
-
-From e15649b83a78f89f57205927022115536d2c1698 Mon Sep 17 00:00:00 2001
-From: Eli Schwartz <eschwartz93@gmail.com>
-Date: Tue, 21 Jun 2022 20:18:22 -0400
-Subject: [PATCH] make the meson.build stub a bit more well-rounded by
- exporting files
-
-Provide variables for:
-- the found program keymap-gen
-- the CSV mapping table
-
-and for enhanced convenience, override keymap-gen
-
-This allows grabbing the variables from another Meson project without
-futzing with submodule paths, something that Meson doesn't really
-encourage.
----
- meson.build | 7 ++++++-
- 1 file changed, 6 insertions(+), 1 deletion(-)
-
-diff --git a/subprojects/keycodemapdb/meson.build b/subprojects/keycodemapdb/meson.build
-index eb9416b..6d263aa 100644
---- a/subprojects/keycodemapdb/meson.build
-+++ b/subprojects/keycodemapdb/meson.build
-@@ -1 +1,6 @@
--project('keycodemapdb')
-+project('keycodemapdb', meson_version: '>=0.46.0')
-+
-+keymap_gen = find_program('tools/keymap-gen')
-+meson.override_find_program('keymap-gen', keymap_gen)
-+
-+keymaps_csv = files('data/keymaps.csv')
--- 
-GitLab
+2.41.0
 
-From d5dc89146697d075178fa916253e2a69a25964b8 Mon Sep 17 00:00:00 2001
+From f648e0730b8ddbb03f2f9e45c121a5bbcc3ba00f Mon Sep 17 00:00:00 2001
 From: osy <osy@turing.llc>
 Date: Sun, 6 Aug 2023 01:11:31 -0700
 Subject: [PATCH] meson: disable version script
@@ -1445,7 +911,7 @@ Fails to build on Xcode 15
  1 file changed, 1 insertion(+), 1 deletion(-)
 
 diff --git a/src/meson.build b/src/meson.build
-index 961779f..5ef1e0d 100644
+index daff1aa..61e60fa 100644
 --- a/src/meson.build
 +++ b/src/meson.build
 @@ -205,7 +205,7 @@ spice_client_glib_lib = library('spice-client-glib-2.0', spice_client_glib_sourc

+ 5 - 3
scripts/build_dependencies.sh

@@ -208,8 +208,10 @@ generate_meson_cross() {
     echo "[built-in options]" >> $cross
     echo "c_args = [${CFLAGS:+$(meson_quote $CFLAGS)}]" >> $cross
     echo "cpp_args = [${CXXFLAGS:+$(meson_quote $CXXFLAGS)}]" >> $cross
+    echo "objc_args = [${CFLAGS:+$(meson_quote $CFLAGS)}]" >> $cross
     echo "c_link_args = [${LDFLAGS:+$(meson_quote $LDFLAGS)}]" >> $cross
     echo "cpp_link_args = [${LDFLAGS:+$(meson_quote $LDFLAGS)}]" >> $cross
+    echo "objc_link_args = [${LDFLAGS:+$(meson_quote $LDFLAGS)}]" >> $cross
     echo "[binaries]" >> $cross
     echo "c = [$(meson_quote $CC)]" >> $cross
     echo "cpp = [$(meson_quote $CXX)]" >> $cross
@@ -464,7 +466,7 @@ build_qemu_dependencies () {
     build $GETTEXT_SRC --disable-java
     build $PNG_SRC
     build $JPEG_TURBO_SRC
-    meson_build $GLIB_SRC -Dtests=false
+    meson_build $GLIB_SRC -Dtests=false -Ddtrace=disabled
     build $GPG_ERROR_SRC
     build $GCRYPT_SRC
     build $PIXMAN_SRC
@@ -499,9 +501,9 @@ build_spice_client () {
     meson_build $LIBUCONTEXT_REPO -Ddefault_library=static -Dfreestanding=true
     meson_build $JSON_GLIB_SRC -Dintrospection=disabled
     build $XML2_SRC --enable-shared=no --without-python
-    meson_build $SOUP_SRC --default-library static -Dsysprof=disabled -Dtls_check=false -Dintrospection=disabled
+    meson_build $SOUP_SRC -Dsysprof=disabled -Dtls_check=false -Dintrospection=disabled
     meson_build $PHODAV_SRC
-    meson_build $SPICE_CLIENT_SRC -Dcoroutine=libucontext -Dphysical-cd=disabled
+    meson_build $SPICE_CLIENT_SRC -Dcoroutine=libucontext
 }
 
 fixup () {

+ 22 - 0
utmctl/UTMCtl.swift

@@ -36,6 +36,7 @@ struct UTMCtl: ParsableCommand {
             IPAddress.self,
             Clone.self,
             Delete.self,
+            Export.self,
             USB.self
         ]
     )
@@ -522,6 +523,27 @@ extension UTMCtl {
     }
 }
 
+extension UTMCtl {
+    struct Export: UTMAPICommand {
+        static var configuration =  CommandConfiguration(
+            abstract: "Export a virtual machine and all its data to a specified location."
+        )
+        
+        @OptionGroup var environment: EnvironmentOptions
+        
+        @OptionGroup var identifer: VMIdentifier
+        
+        @Option var path: String
+        
+        func run(with application: UTMScriptingApplication) throws {
+            let vm = try virtualMachine(forIdentifier: identifer, in: application)
+            // TODO: Make sure the URL is writable as required by data.export
+            let exportUrl = URL(fileURLWithPath: path)
+            vm.exportTo!(exportUrl)
+        }
+    }
+}
+
 extension UTMCtl {
     struct USB: ParsableCommand {
         static var configuration = CommandConfiguration(

+ 1 - 1
utmctl/utmctl-unsigned.entitlements

@@ -3,7 +3,7 @@
 <plist version="1.0">
 <dict>
 	<key>com.apple.security.app-sandbox</key>
-	<false/>
+	<true/>
 	<key>com.apple.security.network.client</key>
 	<true/>
 	<key>com.apple.security.scripting-targets</key>

Some files were not shown because too many files changed in this diff