diff --git a/Configuration/UTMAppleConfiguration.swift b/Configuration/UTMAppleConfiguration.swift index 08c8c206a..6b6fea738 100644 --- a/Configuration/UTMAppleConfiguration.swift +++ b/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 diff --git a/Configuration/UTMAppleConfigurationSystem.swift b/Configuration/UTMAppleConfigurationSystem.swift index a1f508162..c16a01294 100644 --- a/Configuration/UTMAppleConfigurationSystem.swift +++ b/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 { diff --git a/Configuration/UTMAppleConfigurationVirtualization.swift b/Configuration/UTMAppleConfigurationVirtualization.swift index 6949c05f9..c8a9618db 100644 --- a/Configuration/UTMAppleConfigurationVirtualization.swift +++ b/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() diff --git a/Configuration/UTMQemuConfiguration+Arguments.swift b/Configuration/UTMQemuConfiguration+Arguments.swift index 6f8e2ab6e..8d104a7c1 100644 --- a/Configuration/UTMQemuConfiguration+Arguments.swift +++ b/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") diff --git a/Configuration/UTMQemuConfigurationQEMU.swift b/Configuration/UTMQemuConfigurationQEMU.swift index 14c6d2619..fba97951b 100644 --- a/Configuration/UTMQemuConfigurationQEMU.swift +++ b/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 diff --git a/Documentation/MacDevelopment.md b/Documentation/MacDevelopment.md index 7a28c8ac2..d924a7563 100644 --- a/Documentation/MacDevelopment.md +++ b/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. diff --git a/Documentation/TetheredLaunch.zh-HK.md b/Documentation/TetheredLaunch.zh-HK.md index 65ed969ab..3b87caa87 100644 --- a/Documentation/TetheredLaunch.zh-HK.md +++ b/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 diff --git a/Documentation/TetheredLaunch.zh-Hans.md b/Documentation/TetheredLaunch.zh-Hans.md index 33bcc235e..0dd0ba30e 100644 --- a/Documentation/TetheredLaunch.zh-Hans.md +++ b/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 diff --git a/Platform/Shared/BusyOverlay.swift b/Platform/Shared/BusyOverlay.swift index 17fc29129..0ef42900c 100644 --- a/Platform/Shared/BusyOverlay.swift +++ b/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)) + } } } } diff --git a/Platform/Shared/ContentView.swift b/Platform/Shared/ContentView.swift index d0cca7edc..350ac271a 100644 --- a/Platform/Shared/ContentView.swift +++ b/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() diff --git a/Platform/Shared/VMContextMenuModifier.swift b/Platform/Shared/VMContextMenuModifier.swift index 066f5dfd1..27b761d81 100644 --- a/Platform/Shared/VMContextMenuModifier.swift +++ b/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 } } diff --git a/Platform/Shared/VMWizardState.swift b/Platform/Shared/VMWizardState.swift index 48d9dbfc2..f5a935910 100644 --- a/Platform/Shared/VMWizardState.swift +++ b/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: diff --git a/Platform/UTMData.swift b/Platform/UTMData.swift index 271d7c710..764be0263 100644 --- a/Platform/UTMData.swift +++ b/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) } } } diff --git a/Platform/UTMDownloadMacSupportToolsTask.swift b/Platform/UTMDownloadMacSupportToolsTask.swift new file mode 100644 index 000000000..5090f9584 --- /dev/null +++ b/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 + } +} diff --git a/Platform/iOS/Display/zh-HK.lproj/VMDisplayMetalViewInputAccessory.strings b/Platform/iOS/Display/zh-HK.lproj/VMDisplayMetalViewInputAccessory.strings index 7a91647a2..08effe1ed 100644 --- a/Platform/iOS/Display/zh-HK.lproj/VMDisplayMetalViewInputAccessory.strings +++ b/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"; diff --git a/Platform/iOS/zh-HK.lproj/Info-RemotePlist.strings b/Platform/iOS/zh-HK.lproj/Info-RemotePlist.strings index 6387088a3..e6ce4f687 100644 --- a/Platform/iOS/zh-HK.lproj/Info-RemotePlist.strings +++ b/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" = "任何虛擬機器都需要權限才能從咪高風錄製。"; diff --git a/Platform/iOS/zh-HK.lproj/InfoPlist.strings b/Platform/iOS/zh-HK.lproj/InfoPlist.strings index 89af126d7..d6e3fe80f 100644 --- a/Platform/iOS/zh-HK.lproj/InfoPlist.strings +++ b/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 虛擬機器"; diff --git a/Platform/iOS/zh-Hans.lproj/Info-Remote-InfoPlist.strings b/Platform/iOS/zh-Hans.lproj/Info-Remote-InfoPlist.strings index 3112abb40..99779b565 100644 --- a/Platform/iOS/zh-Hans.lproj/Info-Remote-InfoPlist.strings +++ b/Platform/iOS/zh-Hans.lproj/Info-Remote-InfoPlist.strings @@ -5,5 +5,5 @@ "NSLocalNetworkUsageDescription" = "UTM 使用本地网络查找和连接 UTM 远程服务器。"; /* Privacy - Microphone Usage Description */ -"NSMicrophoneUsageDescription" = "任何虚拟机都需要获得许可才能从麦克风录音。"; +"NSMicrophoneUsageDescription" = "任何虚拟机都需要权限才能通过麦克风录音。"; diff --git a/Platform/iOS/zh-Hans.lproj/Info-RemotePlist.strings b/Platform/iOS/zh-Hans.lproj/Info-RemotePlist.strings index 47c3753f6..73e4e335f 100644 --- a/Platform/iOS/zh-Hans.lproj/Info-RemotePlist.strings +++ b/Platform/iOS/zh-Hans.lproj/Info-RemotePlist.strings @@ -5,5 +5,5 @@ "NSLocalNetworkUsageDescription" = "UTM 使用本地网络查找并连接到 UTM 远程服务器。"; /* Privacy - Microphone Usage Description */ -"NSMicrophoneUsageDescription" = "任何虚拟机都需要获得许可才能从麦克风录音。"; +"NSMicrophoneUsageDescription" = "任何虚拟机都需要权限才能通过麦克风录音。"; diff --git a/Platform/iOS/zh-Hans.lproj/InfoPlist.strings b/Platform/iOS/zh-Hans.lproj/InfoPlist.strings index 5a88d0dca..0a502753e 100644 --- a/Platform/iOS/zh-Hans.lproj/InfoPlist.strings +++ b/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 虚拟机"; diff --git a/Platform/macOS/Display/VMDisplayAppleDisplayWindowController.swift b/Platform/macOS/Display/VMDisplayAppleDisplayWindowController.swift index 678d98b5a..6fdaa6ecb 100644 --- a/Platform/macOS/Display/VMDisplayAppleDisplayWindowController.swift +++ b/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 + } +} diff --git a/Platform/macOS/Display/VMDisplayAppleWindowController.swift b/Platform/macOS/Display/VMDisplayAppleWindowController.swift index 8b2c3bcd2..584053dac 100644 --- a/Platform/macOS/Display/VMDisplayAppleWindowController.swift +++ b/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() { diff --git a/Platform/macOS/Display/VMDisplayQemuMetalWindowController.swift b/Platform/macOS/Display/VMDisplayQemuMetalWindowController.swift index 9c346b992..4e08638aa 100644 --- a/Platform/macOS/Display/VMDisplayQemuMetalWindowController.swift +++ b/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 { diff --git a/Platform/macOS/Display/zh-Hans.lproj/VMDisplayWindow.strings b/Platform/macOS/Display/zh-Hans.lproj/VMDisplayWindow.strings index c84d01409..635c5bb3a 100644 --- a/Platform/macOS/Display/zh-Hans.lproj/VMDisplayWindow.strings +++ b/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" = "启动/恢复虚拟机"; diff --git a/Platform/macOS/UTMDataExtension.swift b/Platform/macOS/UTMDataExtension.swift index edb1ce98c..fa5f83dd4 100644 --- a/Platform/macOS/UTMDataExtension.swift +++ b/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")) } } } diff --git a/Platform/macOS/VMAppleRemovableDrivesView.swift b/Platform/macOS/VMAppleRemovableDrivesView.swift index 7b262f0a7..f834999cd 100644 --- a/Platform/macOS/VMAppleRemovableDrivesView.swift +++ b/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) { 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) + } + } } } diff --git a/Platform/macOS/VMConfigAppleVirtualizationView.swift b/Platform/macOS/VMConfigAppleVirtualizationView.swift index 152de1d70..1a197c7cb 100644 --- a/Platform/macOS/VMConfigAppleVirtualizationView.swift +++ b/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.") } diff --git a/Platform/macOS/zh-HK.lproj/InfoPlist.strings b/Platform/macOS/zh-HK.lproj/InfoPlist.strings index c39a8cf23..0569603c3 100644 --- a/Platform/macOS/zh-HK.lproj/InfoPlist.strings +++ b/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 虛擬機器"; diff --git a/Platform/macOS/zh-Hans.lproj/InfoPlist.strings b/Platform/macOS/zh-Hans.lproj/InfoPlist.strings index fca34c71f..1e25c15b5 100644 --- a/Platform/macOS/zh-Hans.lproj/InfoPlist.strings +++ b/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 虚拟机"; diff --git a/Platform/zh-HK.lproj/Localizable.strings b/Platform/zh-HK.lproj/Localizable.strings index 69dcd569d..3c7b3e173 100644 --- a/Platform/zh-HK.lproj/Localizable.strings +++ b/Platform/zh-HK.lproj/Localizable.strings @@ -39,7 +39,7 @@ "%@ remaining" = "剩餘時間 %@"; /* Format string for the 'per second' part of a download speed. */ -"%@/s" = "%@/s"; +"%@/s" = "%@/秒"; /* Format string for download progress and speed, e. g. 5 MB of 6 GB (200 kbit/s) */ "%1$@ of %2$@ (%3$@)" = "%1$@ / %2$@ (%3$@)"; @@ -63,25 +63,25 @@ "A valid configuration must be specified." = "必須指定有效的設定。"; /* UTMAppleConfiguration */ -"A valid kernel image must be specified." = "必須指定有效的內核映像檔。"; +"A valid kernel image must be specified." = "必須指定有效的核心映像檔。"; /* VMDisplayAppleController */ "Add…" = "新增⋯"; /* No comment provided by engineer. */ -"Additional Options" = "附加選項"; +"Additional Options" = "額外選項"; /* No comment provided by engineer. */ -"Additional Settings" = "附加設定"; +"Additional Settings" = "額外設定"; /* VMConfigSystemView */ -"Allocating too much memory will crash the VM." = "分配過多記憶體會使虛擬電腦當機。"; +"Allocating too much memory will crash the VM." = "分配過多的記憶體會使虛擬機器故障。"; /* UTMData */ "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." = "發生內部錯誤。"; @@ -90,7 +90,7 @@ "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,25 +102,25 @@ "ANGLE (OpenGL)" = "ANGLE (OpenGL)"; /* VMConfigSystemView */ -"Any unsaved changes will be lost." = "任何未儲存的變更都將遺失。"; +"Any unsaved changes will be lost." = "任何未儲存的變更將會遺失。"; /* No comment provided by engineer. */ -"Approve" = "批准"; +"Approve" = "核准"; /* No comment provided by engineer. */ -"Architecture" = "體系結構"; +"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" = "認證"; @@ -142,7 +142,7 @@ "Block" = "封鎖"; /* No comment provided by engineer. */ -"Blocked" = "已經封鎖"; +"Blocked" = "已封鎖"; /* UTMQemuConstants */ "Bold" = "粗體"; @@ -151,7 +151,7 @@ "Boot" = "啟動"; /* No comment provided by engineer. */ -"Boot Arguments" = "啟動參數"; +"Boot Arguments" = "啟動引數"; /* No comment provided by engineer. */ "Boot Image Type" = "啟動映像檔種類"; @@ -175,7 +175,7 @@ "Bridged Settings" = "橋連設定"; /* Welcome view */ -"Browse UTM Gallery" = "瀏覽 UTM 虛擬電腦庫"; +"Browse UTM Gallery" = "瀏覽 UTM 虛擬機器庫"; /* No comment provided by engineer. */ "Browse…" = "瀏覽⋯"; @@ -184,10 +184,10 @@ "Build" = "構建"; /* UTMQemuConstants */ -"Built-in Terminal" = "預置終端機"; +"Built-in Terminal" = "內置終端機"; /* No comment provided by engineer. */ -"Busy…" = "正忙⋯"; +"Busy…" = "忙碌中⋯"; /* VMDisplayWindowController VMQemuDisplayMetalWindowController */ @@ -200,19 +200,19 @@ "Cannot access TPM data." = "無法取用 TPM 資料。"; /* UTMAppleVirtualMachine */ -"Cannot create virtual terminal." = "無法製作虛擬終端機。"; +"Cannot create virtual terminal." = "無法建立虛擬終端機。"; /* UTMData */ -"Cannot find AltServer for JIT enable. You cannot run VMs until JIT is enabled." = "無法找到 JIT 啟用的 AltServer。在啟用 JIT 之前,你無法執行虛擬電腦。"; +"Cannot find AltServer for JIT enable. You cannot run VMs until JIT is enabled." = "無法找到 JIT 啟用的 AltServer。在啟用 JIT 之前,你無法執行虛擬機器。"; /* UTMRemoteServer */ -"Cannot find VM with ID: %@" = "無法由這個 ID 尋找虛擬電腦:"; +"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." = "無法為 NAT 外部取用保留埠「%d」。請確保網絡上未有其他裝置保留它。"; +"Cannot reserve port %d for external access from NAT. Make sure no other device on the network has reserved it." = "無法為 NAT 外部取用保留埠「%d」。確保網絡上未有其他裝置保留它。"; /* No comment provided by engineer. */ "Caps Lock (⇪) is treated as a key" = "將 Caps Lock (⇪) 視為按鍵"; @@ -224,7 +224,7 @@ "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" = "已擷取滑鼠"; @@ -246,7 +246,7 @@ "Change…" = "變更⋯"; /* No comment provided by engineer. */ -"Choose" = "選取"; +"Choose" = "選擇"; /* No comment provided by engineer. */ "Clear" = "清除"; @@ -255,7 +255,7 @@ "Close" = "關閉"; /* VMQemuDisplayMetalWindowController */ -"Closing this window will kill the VM." = "關閉這個視窗會結束虛擬電腦。"; +"Closing this window will kill the VM." = "關閉這個視窗會結束虛擬機器。"; /* VMQemuDisplayMetalWindowController */ "Confirm" = "確認"; @@ -271,13 +271,13 @@ "Connect" = "連接"; /* No comment provided by engineer. */ -"Connected" = "已經連接"; +"Connected" = "已連接"; /* No comment provided by engineer. */ "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" = "核心"; @@ -301,13 +301,13 @@ "Create" = "製作"; /* No comment provided by engineer. */ -"Create a new emulated machine from scratch." = "從頭開始製作一個新的虛擬電腦。"; +"Create a new emulated machine from scratch." = "從頭開始製作新的虛擬機器。"; /* Welcome view */ -"Create a New Virtual Machine" = "製作一個新虛擬電腦"; +"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" = "自訂"; @@ -344,7 +344,7 @@ "Disconnect" = "中斷連接"; /* No comment provided by engineer. */ -"Discovered" = "已經中斷連接"; +"Discovered" = "已偵測到"; /* UTMLegacyQemuConfiguration UTMQemuConstants */ @@ -360,37 +360,37 @@ "Disposable Mode" = "即拋式模式"; /* No comment provided by engineer. */ -"Do not save VM screenshot to disk" = "不要儲存虛擬電腦的螢幕截圖至磁碟"; +"Do not save VM screenshot to disk" = "不要儲存虛擬機器的螢幕截圖至磁碟"; /* No comment provided by engineer. */ -"Do not show confirmation when closing a running VM" = "關閉正在執行的虛擬電腦時不要顯示確認"; +"Do not show confirmation when closing a running VM" = "關閉正在執行的虛擬機器時不要顯示確認"; /* No comment provided by engineer. */ "Do not show prompt when USB device is plugged in" = "插入 USB 裝置時不要顯示提示"; /* 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 '%@'?" = "你要下載「%@」嗎?"; /* 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." = "你要刪除這個捷徑嗎?資料不會被刪除。"; @@ -399,10 +399,10 @@ "Download" = "下載"; /* No comment provided by engineer. */ -"Download prebuilt from UTM Gallery…" = "從 UTM 虛擬電腦庫下載預構建⋯"; +"Download prebuilt from UTM Gallery…" = "從 UTM 虛擬機器庫下載預構建⋯"; /* No comment provided by engineer. */ -"Download VM" = "下載虛擬電腦"; +"Download VM" = "下載虛擬機器"; /* No comment provided by engineer. */ "Drag and drop IPSW file here" = "拖放 IPSW 檔至這裡"; @@ -429,13 +429,13 @@ "Enable Clipboard Sharing" = "啟用剪貼板分享"; /* No comment provided by engineer. */ -"Enjoying the app? Consider making a donation to support development." = "享受使用這個 App 嗎?考慮下捐贈支持開發吧。"; +"Enjoying the app? Consider making a donation to support development." = "享受使用這個 App 嗎?考慮下捐贈以支持開發吧。"; /* VMDisplayWindowController */ "Error" = "錯誤"; /* No comment provided by engineer. */ -"Existing" = "現存"; +"Existing" = "現有"; /* No comment provided by engineer. */ "Export QEMU Command…" = "輸出 QEMU 指令⋯"; @@ -444,7 +444,7 @@ "Extracting…" = "正在解壓縮⋯"; /* UTMQemuVirtualMachine */ -"Failed to access data from shortcut." = "無法由捷徑取用資料。"; +"Failed to access data from shortcut." = "無法從捷徑取用資料。"; /* UTMQemuVirtualMachine */ "Failed to access drive image path." = "無法取用磁碟映像檔路徑。"; @@ -462,10 +462,10 @@ "Failed to attach to JitStreamer." = "無法附加至 JitStreamer。"; /* UTMSpiceIO */ -"Failed to change current directory." = "無法變更當前目錄。"; +"Failed to change current directory." = "無法變更現時的目錄。"; /* UTMData */ -"Failed to clone VM." = "無法複製虛擬電腦。"; +"Failed to clone VM." = "無法複製虛擬機器。"; /* UTMRemoteSpiceVirtualMachine */ "Failed to connect to SPICE: %@" = "無法連接至 SPICE:%@"; @@ -489,22 +489,22 @@ "Failed to get host fingerprint." = "無法取得主機指紋。"; /* VMWizardState */ -"Failed to get latest macOS version from Apple." = "無法由 Apple 取得最新的 macOS 版本。"; +"Failed to get latest macOS version from Apple." = "無法從 Apple 取得最新的 macOS 版本。"; /* UTMRemoteKeyManager */ -"Failed to import generated key." = "無法輸入已經生成的密鑰。"; +"Failed to import generated key." = "無法輸入已生成的密鑰。"; /* UTMQemuConfigurationError */ -"Failed to migrate configuration from a previous UTM version." = "無法由之前版本的 UTM 轉移設定。"; +"Failed to migrate configuration from a previous UTM version." = "無法從之前版本的 UTM 轉移設定。"; /* UTMRemoteKeyManager */ "Failed to parse generated key pair." = "無法解析生成的密鑰對。"; /* UTMData */ -"Failed to parse imported VM." = "無法解析已經輸入的虛擬電腦。"; +"Failed to parse imported VM." = "無法解析已輸入的虛擬機器。"; /* UTMDownloadVMTask */ -"Failed to parse the downloaded VM." = "無法解析已經下載的虛擬電腦。"; +"Failed to parse the downloaded VM." = "無法解析已下載的虛擬機器。"; /* UTMData */ "Failed to reconnect to the server." = "無法重新連接至伺服器。"; @@ -514,13 +514,13 @@ "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 客戶端。"; +"Failed to start SPICE client." = "無法啟動 SPICE 客户端。"; /* No comment provided by engineer. */ -"Faster, but can only run the native CPU architecture." = "較快,但只能執行原生 CPU 體系結構。"; +"Faster, but can only run the native CPU architecture." = "較快,但只限執行原生 CPU 架構。"; /* No comment provided by engineer. */ "Fingerprint" = "指紋"; @@ -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" = "強行多核心"; @@ -548,7 +548,7 @@ "Force shut down" = "強行關機"; /* UTMQemuConstants */ -"GDB Debug Stub" = "GDB Debug Stub"; +"GDB Debug Stub" = "GDB 除錯空函式"; /* No comment provided by engineer. */ "Generic" = "一般"; @@ -566,7 +566,7 @@ "GiB" = "GiB"; /* No comment provided by engineer. */ -"Guest drivers are required for 3D acceleration." = "需要客戶端驅動程式才能使用 3D 加速。"; +"Guest drivers are required for 3D acceleration." = "需要客户端驅動程式才能使用 3D 加速。"; /* Configuration boot device */ "Hard Disk" = "硬碟"; @@ -581,13 +581,13 @@ "Hide Unused…" = "隱藏未使用的⋯"; /* No comment provided by engineer. */ -"Hold Control (⌃) for right click" = "按住 Control (⌃) 來右鍵點按"; +"Hold Control (⌃) for right click" = "按住 Control (⌃) 以右鍵點按"; /* No comment provided by engineer. */ "Host" = "主機"; /* UTMQemuConstants */ -"Host Only" = "僅主機"; +"Host Only" = "只限主機"; /* No comment provided by engineer. */ "Hostname or IP address" = "主機或 IP 位址"; @@ -623,7 +623,7 @@ "Information" = "訊息"; /* VMDisplayWindowController */ -"Install Windows Guest Tools…" = "安裝 Windows 客戶端工具⋯"; +"Install Windows Guest Tools…" = "安裝 Windows 客户端工具⋯"; /* VMDisplayAppleWindowController */ "Installation: %@" = "安裝:%@"; @@ -659,7 +659,7 @@ "IP Configuration" = "IP 設定"; /* No comment provided by engineer. */ -"Isolate Guest from Host" = "將客戶端與主機隔離"; +"Isolate Guest from Host" = "從主機隔離客户端"; /* UTMQemuConstants */ "Italic" = "斜體"; @@ -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" = "許可協議"; @@ -716,7 +716,7 @@ "macOS" = "macOS"; /* VMWizardOSMacView */ -"macOS guests are only supported on ARM64 devices." = "macOS 客戶端僅支援 ARM64 裝置。"; +"macOS guests are only supported on ARM64 devices." = "macOS 客户端只限支援 ARM64 裝置。"; /* VMWizardState */ "macOS is not supported with QEMU." = "macOS 不支援 QEMU。"; @@ -725,7 +725,7 @@ "macOS Settings" = "macOS 設定"; /* No comment provided by engineer. */ -"Make sure the latest version of UTM is running on your Mac and UTM Server is enabled. You can download UTM from the Mac App Store." = "確保最新版本的 UTM 在你的 Mac 上執行,並且已經啟用 UTM 伺服器。你可以透過 Mac App Store下載 UTM。"; +"Make sure the latest version of UTM is running on your Mac and UTM Server is enabled. You can download UTM from the Mac App Store." = "確保最新版本的 UTM 在你的 Mac 上執行,並且已啟用 UTM 伺服器。你可以透過 Mac App Store 下載 UTM。"; /* UTMQemuConstants */ "Manual Serial Device (advanced)" = "手動序列裝置 (進階)"; @@ -743,7 +743,7 @@ "MiB" = "MiB"; /* No comment provided by engineer. */ -"Minimum size: %@" = "最小大小:%@"; +"Minimum size: %@" = "大小最小值:%@"; /* UTMDonateView */ "month" = "月"; @@ -788,19 +788,19 @@ "No" = "否"; /* UTMScriptingAppDelegate */ -"No architecture specified in the configuration." = "設定內未指定體系結構。"; +"No architecture specified in the configuration." = "設定當中未指定架構。"; /* VMDisplayWindowController */ -"No drives connected." = "未有已經連接的磁碟。"; +"No drives connected." = "未有已連接的磁碟。"; /* UTMDownloadSupportToolsTaskError */ "No empty removable drive found. Make sure you have at least one removable drive that is not in use." = "無法找到空的可移除式磁碟。確保你至少有一個未使用的可移除式磁碟。"; /* UTMScriptingAppDelegate */ -"No name specified in the configuration." = "設定內未指定名稱。"; +"No name specified in the configuration." = "設定當中未指定名稱。"; /* No comment provided by engineer. */ -"No output device is selected for this window." = "在這個視窗內未選取任何輸出裝置。"; +"No output device is selected for this window." = "未對這個視窗選擇任何輸出裝置。"; /* No comment provided by engineer. */ "No release notes found for version %@." = "無法找到版本 %@ 的發行註記。"; @@ -809,7 +809,7 @@ "No USB devices detected." = "未偵測到 USB 裝置。"; /* No comment provided by engineer. */ -"No virtual machines found." = "未找到虛擬電腦。"; +"No virtual machines found." = "未找到虛擬機器。"; /* VMToolbarDriveMenuView */ "none" = "無"; @@ -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…" = "開啟⋯"; @@ -876,7 +876,7 @@ "Pause" = "暫停"; /* VMData */ -"Paused" = "已經暫停"; +"Paused" = "已暫停"; /* VMData */ "Pausing" = "正在暫停"; @@ -885,7 +885,7 @@ "PC System Flash" = "PC 系統快閃記憶體"; /* No comment provided by engineer. */ -"Pending" = "待定"; +"Pending" = "待處理"; /* UTMDonateView */ "period" = "期間"; @@ -894,16 +894,16 @@ "Play" = "播放"; /* VMWizardState */ -"Please select a boot image." = "請選取一個啟動檔。"; +"Please select a boot image." = "請選擇一個啟動檔。"; /* VMWizardState */ -"Please select a kernel file." = "請選取一個內核檔。"; +"Please select a kernel file." = "請選擇一個核心檔。"; /* No comment provided by engineer. */ -"Please select a macOS recovery IPSW." = "請選取 IPSW 恢復檔。"; +"Please select a macOS recovery IPSW." = "請選擇一個 IPSW 恢復檔。"; /* No comment provided by engineer. */ -"Please select an uncompressed Linux kernel image." = "請選取未壓縮的 Linux 核心映像檔。"; +"Please select an uncompressed Linux kernel image." = "請選擇一個未壓縮的 Linux 核心映像檔。"; /* No comment provided by engineer. */ "Port" = "埠"; @@ -918,16 +918,16 @@ "Preparing…" = "正在準備⋯"; /* VMDisplayQemuMetalWindowController */ -"Press %@ to release cursor" = "按下 %@ 來放開指標"; +"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 裝置"; /* No comment provided by engineer. */ -"QEMU Arguments" = "QEMU 參數"; +"QEMU Arguments" = "QEMU 引數"; /* No comment provided by engineer. */ "QEMU Graphics Acceleration" = "QEMU 圖形加速"; @@ -954,7 +954,7 @@ "Querying USB devices..." = "正在搜尋 USB 裝置⋯"; /* VMQemuDisplayMetalWindowController */ -"Quitting UTM will kill all running VMs." = "退出 UTM 會結束所有執行的虛擬電腦。"; +"Quitting UTM will kill all running VMs." = "退出 UTM 會結束所有執行的虛擬機器。"; /* No comment provided by engineer. */ "Raw Image" = "Raw 映像檔"; @@ -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" = "正在還原"; @@ -1011,19 +1011,19 @@ "Retina Mode" = "Retina 模式"; /* UTMAppleConfiguration */ -"Rosetta is not supported on the current host machine." = "當前主機不支援 Rosetta。"; +"Rosetta is not supported on the current host machine." = "現時主機不支援 Rosetta。"; /* No comment provided by engineer. */ "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" = "儲存"; /* No comment provided by engineer. */ -"Saved" = "已經存儲"; +"Saved" = "已存儲"; /* VMData */ "Saving" = "正在儲存"; @@ -1038,32 +1038,32 @@ "SD Card" = "SD 卡"; /* No comment provided by engineer. */ -"Select a file." = "選取一個檔案。"; +"Select a file." = "選擇一個檔案。"; /* No comment provided by engineer. */ -"Select a UTM Server" = "選取一個 UTM 伺服器"; +"Select a UTM Server" = "選擇一個 UTM 伺服器"; /* VMDisplayWindowController */ -"Select Drive Image" = "選取磁碟映像檔"; +"Select Drive Image" = "選擇磁碟映像檔"; /* VMDisplayAppleWindowController VMDisplayWindowController */ -"Select Shared Folder" = "選取分享的資料夾"; +"Select Shared Folder" = "選擇分享的資料夾"; /* SavePanel */ -"Select where to export QEMU command:" = "選取輸出 QEMU 指令的位置:"; +"Select where to export QEMU command:" = "選擇輸出 QEMU 指令的位置:"; /* SavePanel */ -"Select where to save debug log:" = "選取儲存除錯記錄的位置:"; +"Select where to save debug log:" = "選擇儲存除錯記錄的位置:"; /* SavePanel */ -"Select where to save UTM Virtual Machine:" = "選取儲存 UTM 虛擬電腦的位置:"; +"Select where to save UTM Virtual Machine:" = "選擇儲存 UTM 虛擬機器的位置:"; /* No comment provided by engineer. */ -"Selected:" = "已選取:"; +"Selected:" = "已選擇:"; /* VMDisplayWindowController */ -"Sends power down request to the guest. This simulates pressing the power button on a PC." = "向客戶端發送關閉電源請求。這個操作仿真了按下 PC 上的電源按鈕。"; +"Sends power down request to the guest. This simulates pressing the power button on a PC." = "向客户端發送關閉電源請求。這個操作仿真了按一下 PC 上的電源按鈕。"; /* VMDisplayAppleWindowController VMDisplayQemuDisplayController */ @@ -1079,7 +1079,7 @@ "Share USB devices from host" = "從主機分享 USB 裝置"; /* No comment provided by engineer. */ -"Shared directories in macOS VMs are only available in macOS 13 and later." = "macOS 虛擬電腦分享目錄僅在 macOS 13 及更高版本可用。"; +"Shared directories in macOS VMs are only available in macOS 13 and later." = "macOS 虛擬機器分享目錄只限 macOS 13 和以上版本可用。"; /* No comment provided by engineer. */ "Shared Directory" = "已分享目錄"; @@ -1109,10 +1109,10 @@ "Size" = "大小"; /* No comment provided by engineer. */ -"Slower, but can run other CPU architectures." = "較慢,但可以執行其他 CPU 體系結構。"; +"Slower, but can run other CPU architectures." = "較慢,但可以執行其他 CPU 架構。"; /* UTMSWTPM */ -"Socket not specified." = "未指定 socket。"; +"Socket not specified." = "未指定 Socket。"; /* No comment provided by engineer. */ "Specify the size of the drive where data will be stored into." = "指定資料儲存至的磁碟的大小。"; @@ -1124,10 +1124,10 @@ "SPICE with GStreamer (Input & Output)" = "SPICE with GStreamer (輸入與輸出)"; /* No comment provided by engineer. */ -"Start Here" = "由這裡開始"; +"Start Here" = "從這裡開始"; /* VMData */ -"Started" = "已經啟動"; +"Started" = "已啟動"; /* VMData */ "Starting" = "正在啟動"; @@ -1160,13 +1160,13 @@ "Suspend is not supported for virtualization." = "暫停功能不支援虛擬化。"; /* UTMQemuVirtualMachine */ -"Suspend is not supported when an emulated NVMe device is active." = "当仿真 NVMe 裝置處於啟用狀態時,不支援暫停功能。"; +"Suspend is not supported when an emulated NVMe device is active." = "仿真 NVMe 裝置活動期間無法支援暫停功能。"; /* UTMQemuVirtualMachine */ -"Suspend is not supported when GPU acceleration is enabled." = "当 GPU 加速處於啟用狀態時,不支援暫停功能。"; +"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" = "暫停"; @@ -1187,13 +1187,13 @@ "TCP" = "TCP"; /* UTMQemuConstants */ -"TCP Client Connection" = "TCP 客戶端連線"; +"TCP Client Connection" = "TCP 客户端連線"; /* UTMQemuConstants */ "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" = "測試"; @@ -1208,58 +1208,58 @@ "The backend for this configuration is not supported." = "不支援這個設定的後端。"; /* UTMRemoteServer */ -"The client interface version does not match the server." = "客戶端介面版本與伺服器不匹配。"; +"The client interface version does not match the server." = "客户端介面版本與伺服器不匹配。"; /* UTMScriptingUSBDeviceImpl */ "The device cannot be found." = "無法找到這個裝置。"; /* UTMScriptingUSBDeviceImpl */ -"The device is not currently connected." = "這個裝置當前未連接。"; +"The device is not currently connected." = "現時這個裝置未連接。"; /* UTMConfiguration */ -"The drive '%@' already exists and cannot be created." = "磁碟「%@」已經存在,無法製作。"; +"The drive '%@' already exists and cannot be created." = "磁碟「%@」已存在,無法製作。"; /* UTMDownloadSupportToolsTaskError */ -"The guest support tools have already been mounted." = "這個客戶端支援工具已經被裝載。"; +"The guest support tools have already been mounted." = "這個客户端支援工具被裝載。"; /* 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." = "無法在這個電腦上安裝該作業系統。"; +"The operating system cannot be installed on this machine." = "無法在這個電腦上安裝這個作業系統。"; /* UTMAppleVirtualMachine */ "The operation is not available." = "這個操作不可用。"; /* UTMScriptingVirtualMachineImpl */ -"The QEMU guest agent is not running or not installed on the guest." = "QEMU 客戶端代理程式未執行或未在客戶端上安裝。"; +"The QEMU guest agent is not running or not installed on the guest." = "QEMU 客户端代理程式未執行或未在客户端上安裝。"; /* No comment provided by engineer. */ -"The selected architecture is unsupported in this version of UTM." = "這個版本的 UTM 不支援所選取的體系結構。"; +"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 '%@'." = "選取的啟動映像檔包含單字「%1$@」,但客戶端體系結構為「%2$@」。確保你選取了與「%3$@」體系結構相容的映像檔。"; +"The selected boot image contains the word '%@' but the guest architecture is '%@'. Please ensure you have selected an image that is compatible with '%@'." = "選擇的啟動映像檔包含單字「%1$@」,但客户端架構為「%2$@」。確保你選擇了與「%3$@」架構相容的映像檔。"; /* UTMRemoteClient */ -"The server interface version does not match the client." = "伺服器介面版本與客戶端不匹配。"; +"The server interface version does not match the client." = "伺服器介面版本與客户端不匹配。"; /* No comment provided by engineer. */ "The target does not support hardware emulated serial connections." = "目標平台不支援硬件仿真序列連線。"; /* UTMQemuVirtualMachine */ -"The virtual machine is in an invalid state." = "虛擬電腦處於無效狀態。"; +"The virtual machine is in an invalid state." = "虛擬機器處於無效狀態。"; /* UTMScriptingVirtualMachineImpl */ -"The virtual machine is not running." = "虛擬電腦未在執行。"; +"The virtual machine is not running." = "虛擬機器未在執行。"; /* UTMScriptingVirtualMachineImpl */ -"The virtual machine must be stopped before this operation can be performed." = "必須先停止虛擬電腦,然後才能執行這個操作。"; +"The virtual machine must be stopped before this operation can be performed." = "必須停止虛擬機器才能執行這個操作。"; /* Error shown when importing a ZIP file from web that doesn't contain a UTM Virtual Machine. */ -"There is no UTM file in the downloaded ZIP archive." = "在已經下載的 ZIP 封存檔內未有 UTM 檔案。"; +"There is no UTM file in the downloaded ZIP archive." = "在已下載的 ZIP 封存檔當中未有 UTM 檔案。"; /* No comment provided by engineer. */ "This audio card is not supported." = "這個聲卡不支援。"; @@ -1271,13 +1271,13 @@ "This build does not emulation." = "這個 UTM 構建不支援仿真。"; /* UTMQemuVirtualMachine */ -"This build of UTM does not support emulating the architecture of this VM." = "這個 UTM 構建不支援仿真該虛擬電腦的體系結構。"; +"This build of UTM does not support emulating the architecture of this VM." = "這個 UTM 構建不支援仿真這個虛擬機器的架構。"; /* VMConfigSystemView */ "This change will reset all settings" = "這個變更會重設所有設定"; /* 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." = "這個設定過舊,無法支援。"; @@ -1286,7 +1286,7 @@ "This device is not supported by the target." = "目標不支援這個裝置。"; /* VMConfigAppleSharingView */ -"This directory is already being shared." = "這個目錄已經被分享。"; +"This directory is already being shared." = "這個目錄被分享。"; /* VMData */ "This function is not implemented." = "這個功能未實現。"; @@ -1295,58 +1295,58 @@ "This functionality is not yet implemented." = "這個功能未實現。"; /* UTMRemoteClient */ -"This host is not yet trusted. You should verify that the fingerprints match what is displayed on the host and then select Trust to continue." = "這個主機未得到信任。你應該驗證指紋是否與主機上顯示的內容匹配,然後選擇「信任」以繼續。"; +"This host is not yet trusted. You should verify that the fingerprints match what is displayed on the host and then select Trust to continue." = "這個主機未受信任。你應該驗證這個指紋是否與主機上顯示的內容匹配,然後選擇「信任」以繼續。"; /* UTMAppleConfiguration */ "This is not a valid Apple Virtualization configuration." = "並非有效的 Apple 虛擬化設定。"; /* VMDisplayWindowController */ -"This may corrupt the VM and any unsaved changes will be lost. To quit safely, shut down from the guest." = "這可能會損毀虛擬電腦,任何未儲存的變更都將遺失。如要安全退出,請從客戶端關機。"; +"This may corrupt the VM and any unsaved changes will be lost. To quit safely, shut down from the guest." = "這可能會損毀虛擬機器,任何未儲存的變更都將遺失。如要安全退出,請從客户端關機。"; /* No comment provided by engineer. */ "This operating system is unsupported on your machine." = "你的電腦不支援這個作業系統。"; /* UTMDataExtension */ -"This virtual machine cannot be run on this machine." = "這個虛擬電腦無法在該電腦上執行。"; +"This virtual machine cannot be run on this machine." = "這個虛擬機器無法在這個電腦上執行。"; /* UTMAppleConfiguration */ -"This virtual machine cannot run on the current host machine." = "這個虛擬電腦無法在當前主機上執行。"; +"This virtual machine cannot run on the current host machine." = "這個虛擬機器無法在現時的主機上執行。"; /* UTMAppleConfiguration */ -"This virtual machine contains an invalid hardware model. The configuration may be corrupted or is outdated." = "這個虛擬電腦含有無效的硬體型號。其設定可能損毀或是過時。"; +"This virtual machine contains an invalid hardware model. The configuration may be corrupted or is outdated." = "這個虛擬機器含有無效的硬體型號。其設定可能損毀或過時。"; /* No comment provided by engineer. */ -"This virtual machine has been removed." = "這個虛擬電腦已經被刪除。"; +"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." = "這個虛擬電腦無法使用,或者是被設定為不支援遠端客戶端的後端。"; +"This VM is not available or is configured for a backend that does not support remote clients." = "這個虛擬機器無法使用,或是被設定為不支援遠端客户端的後端。"; /* No comment provided by engineer. */ -"This VM is unavailable." = "這個虛擬電腦無法使用。"; +"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." = "嘗試連接超時。"; /* VMDisplayAppleWindowController */ -"To access the shared directory, the guest OS must have Virtiofs drivers installed. You can then run `sudo mount -t virtiofs share /path/to/share` to mount to the share path." = "要取用分享目錄,客戶端作業系統必須安裝 VirtioFS 驅動程式。然後,你可以執行「sudo mount -t virtiofs share /path/to/share」來裝載至分享路徑。"; +"To access the shared directory, the guest OS must have Virtiofs drivers installed. You can then run `sudo mount -t virtiofs share /path/to/share` to mount to the share path." = "要取用分享目錄,客户端作業系統必須安裝 VirtioFS 驅動程式。然後,你可以執行「sudo mount -t virtiofs share /path/to/share」以裝載至分享路徑。"; /* VMMetalView */ -"To capture input or to release the capture, press Command and Option at the same time." = "要擷取或放開輸入,請同時按下 Command + Option。"; +"To capture input or to release the capture, press Command and Option at the same time." = "要擷取或釋放輸入,請同時按一下 Command + Option。"; /* No comment provided by engineer. */ -"To install macOS, you need to download a recovery IPSW. If you do not select an existing IPSW, the latest macOS IPSW will be downloaded from Apple." = "如要安裝 macOS,你需要下載 IPSW 恢復檔。如你未選取現有的 IPSW,將從 Apple 下載最新的 macOS IPSW。"; +"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" = "信任"; @@ -1361,7 +1361,7 @@ "UEFI" = "UEFI"; /* UTMQemuConfigurationError */ -"UEFI is not supported with this architecture." = "這個體系結構不支援 UEFI。"; +"UEFI is not supported with this architecture." = "這個架構不支援 UEFI。"; /* UTMData */ "Unable to add a shortcut to the new location." = "無法向新位置新增捷徑。"; @@ -1370,7 +1370,7 @@ "Unavailable" = "無法使用"; /* VMWizardState */ -"Unavailable for this platform." = "無法用於這個平台。"; +"Unavailable for this platform." = "無法在這個平台上使用。"; /* No comment provided by engineer. */ "Uncompressed Linux initial ramdisk (optional)" = "未壓縮的 Linux 起始 ramdisk (可選)"; @@ -1400,13 +1400,13 @@ "USB sharing not supported in this build of UTM." = "這個 UTM 構建不支援 USB 分享。"; /* No comment provided by engineer. */ -"Use Command+Option (⌘+⌥) for input capture/release" = "使用 Command + Option (⌘ + ⌥) 來擷取/放開輸入"; +"Use Command+Option (⌘+⌥) for input capture/release" = "使用 Command + Option (⌘ + ⌥) 以擷取/釋放輸入"; /* No comment provided by engineer. */ "Use NVMe Interface" = "使用 NVMe 磁碟介面"; /* Welcome view */ -"User Guide" = "用戶指南"; +"User Guide" = "用户指南"; /* UTMScriptingAppDelegate UTMScriptingUSBDeviceImpl */ @@ -1423,13 +1423,13 @@ /* UTMConfigurationInfo UTMData */ -"Virtual Machine" = "虛擬電腦"; +"Virtual Machine" = "虛擬機器"; /* No comment provided by engineer. */ -"Virtual Machine Gallery" = "虛擬電腦庫"; +"Virtual Machine Gallery" = "虛擬機器庫"; /* VMData */ -"Virtual machine not loaded." = "未載入虛擬電腦。"; +"Virtual machine not loaded." = "未載入虛擬機器。"; /* No comment provided by engineer. */ "Virtualization is not supported on your system." = "你的電腦系統不支援虛擬化。"; @@ -1438,7 +1438,7 @@ "Virtualize" = "虛擬化"; /* No comment provided by engineer. */ -"Waiting for VM to connect to display..." = "正在等待虛擬電腦連接至顯示⋯"; +"Waiting for VM to connect to display..." = "正在等待虛擬機器連接至顯示⋯"; /* UTMDonateView */ "week" = "周"; @@ -1450,22 +1450,22 @@ "What's New" = "新功能"; /* No comment provided by engineer. */ -"When the toolbar is hidden, the icon will disappear after a few seconds. To show the icon again, tap anywhere on the screen." = "當工具列隱藏時,圖示將會於幾秒鐘之後消失。如要再次顯示圖示,請在螢幕上的任意位置點一下。"; +"When the toolbar is hidden, the icon will disappear after a few seconds. To show the icon again, tap anywhere on the screen." = "當工具列隱藏時,圖示將會在幾秒鐘之後消失。如要再次顯示圖示,請在螢幕上的任意位置點一下。"; /* UTMDownloadSupportToolsTask */ -"Windows Guest Support Tools" = "Windows 客戶端支援工具"; +"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,10 +1484,10 @@ "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 版本不支援在未作更動的情況下執行虛擬電腦,必須在越獄 (jailbreak) 時執行 UTM,或是在附加遠程除錯器的情況下執行 UTM。有關更多詳細訊息,請見 https://getutm.app/install/。"; +"Your version of iOS does not support running VMs while unmodified. You must either run UTM while jailbroken or with a remote debugger attached. See https://getutm.app/install/ for more details." = "你的 iOS 版本不支援在未作變更的情況下執行虛擬機器,必須在越獄 (jailbreak) 時執行 UTM,或在附加遠程除錯器的情況下執行 UTM。有關更多詳細訊息,請見 https://getutm.app/install/。"; // Additional Strings (These strings are unable to be extracted by Xcode) @@ -1513,31 +1513,31 @@ "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" = "允許外部客戶端訪問"; +"Allow access from external clients" = "允許外部客户端訪問"; /* No comment provided by engineer. */ "Allow Remote Connection" = "允許遠端連線"; /* No comment provided by engineer. */ -"Allows passing through additional input from trackpads. Only supported on macOS 13+ guests." = "允許透過觸控板額外輸入。僅支援 macOS 13+ 客戶端。"; +"Allows passing through additional input from trackpads. Only supported on macOS 13+ guests." = "允許透過觸控板額外輸入。只限支援 macOS 13+ 客户端。"; /* No comment provided by engineer. */ "Any" = "任意"; /* No comment provided by engineer. */ -"Apple Virtualization is experimental and only for advanced use cases. Leave unchecked to use QEMU, which is recommended." = "Apple 虛擬化為試驗性質,僅可用作進階用例,不選取這個剔選框以使用推介的 QEMU。"; +"Apple Virtualization is experimental and only for advanced use cases. Leave unchecked to use QEMU, which is recommended." = "Apple 虛擬化為試驗性質,只限用作進階用例,不剔選框以使用推介的 QEMU。"; /* No comment provided by engineer. */ "Application" = "應用程式"; /* No comment provided by engineer. */ -"Architecture" = "體系結構"; +"Architecture" = "架構"; /* No comment provided by engineer. */ -"Arguments" = "參數"; +"Arguments" = "引數"; /* No comment provided by engineer. */ "Auto Resolution" = "自動調整解像度"; @@ -1558,16 +1558,16 @@ "Blinking cursor?" = "閃爍指標?"; /* No comment provided by engineer. */ -"Boot arguments" = "啟動參數"; +"Boot arguments" = "啟動引數"; /* No comment provided by engineer. */ -"Boot Arguments" = "啟動參數"; +"Boot Arguments" = "啟動引數"; /* No comment provided by engineer. */ "Boot Device" = "開機裝置"; /* No comment provided by engineer. */ -"Boot from kernel image" = "由核心映像檔啟動"; +"Boot from kernel image" = "從核心映像檔啟動"; /* No comment provided by engineer. */ "Boot Image" = "啟動映像檔"; @@ -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…" = "複製⋯"; @@ -1621,40 +1621,40 @@ "Close" = "關閉"; /* No comment provided by engineer. */ -"Closing a VM without properly shutting it down could result in data loss." = "在未正確關閉電源的情況下關閉虛擬電腦可能會導致資料遺失。"; +"Closing a VM without properly shutting it down could result in data loss." = "在未正確關閉電源的情況下關閉虛擬機器可能會導致資料遺失。"; /* No comment provided by engineer. */ "Compress" = "壓縮"; /* No comment provided by engineer. */ -"Compress by re-converting the disk image and compressing the data." = "透過重新轉換磁碟映像檔與壓縮資料來壓縮。"; +"Compress by re-converting the disk image and compressing the data." = "透過重新轉換磁碟映像檔與壓縮資料以壓縮。"; /* No comment provided by engineer. */ -"Create a new VM" = "製作一個新虛擬電腦"; +"Create a new VM" = "製作一個新虛擬機器"; /* No comment provided by engineer. */ -"Create a new VM with the same configuration as this one but without any data." = "製作一個與這個配置相同的新虛擬電腦,但無任何資料。"; +"Create a new VM with the same configuration as this one but without any data." = "製作一個與這個配置相同的新虛擬機器,但無任何資料。"; /* No comment provided by engineer. */ "Create an empty drive." = "製作一個空磁碟。"; /* No comment provided by engineer. */ -"Debian Install Guide" = "Debian 安裝指引"; +"Debian Install Guide" = "Debian 安裝指南"; /* No comment provided by engineer. */ -"Default is 1/4 of the RAM size (above). The JIT cache size is additive to the RAM size in the total memory usage!" = "預設值為記憶體大小的 1/4 (見上)。JIT 快取資料大小亦會包括於全部的記憶體使用量當中!"; +"Default is 1/4 of the RAM size (above). The JIT cache size is additive to the RAM size in the total memory usage!" = "預設值為記憶體大小的 1/4 (見上)。JIT 快取資料大小亦會包括在全部的記憶體使用量當中!"; /* No comment provided by engineer. */ "Delete this drive." = "刪除這個磁碟。"; /* No comment provided by engineer. */ -"Delete selected VM" = "刪除已選取的虛擬電腦"; +"Delete selected VM" = "刪除已選擇的虛擬機器"; /* No comment provided by engineer. */ "Delete this shortcut. The underlying data will not be deleted." = "刪除這個捷徑。當中的資料不會被刪除。"; /* No comment provided by engineer. */ -"Delete this VM and all its data." = "刪除這個虛擬電腦與其所有資料。"; +"Delete this VM and all its data." = "刪除這個虛擬機器與其所有資料。"; /* No comment provided by engineer. */ "Delete Drive" = "刪除磁碟機"; @@ -1696,13 +1696,13 @@ "Done" = "完成"; /* No comment provided by engineer. */ -"Duplicate this VM along with all its data." = "複製這個虛擬電腦及其所有資料。"; +"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 的客戶端工具。"; +"Download and mount the guest tools for Windows." = "下載並裝載 Windows 的客户端工具。"; /* No comment provided by engineer. */ "Download Windows 11 for ARM64 Preview VHDX" = "下載 Windows 11 ARM64 Preview VHDX 映像檔"; @@ -1717,7 +1717,7 @@ "Edit" = "編輯"; /* No comment provided by engineer. */ -"Edit selected VM" = "編輯已選取的虛擬電腦"; +"Edit selected VM" = "編輯已選擇的虛擬機器"; /* No comment provided by engineer. */ "Edit…" = "編輯⋯"; @@ -1756,7 +1756,7 @@ "Enable Rosetta (x86_64 Emulation)" = "啟用 Rosetta (x86_64 仿真)"; /* No comment provided by engineer. */ -"Enable Rosetta on Linux (x86_64 Emulation)" = "於 Linux 中啟用 Rosetta (x86_64 仿真)"; +"Enable Rosetta on Linux (x86_64 Emulation)" = "在 Linux 中啟用 Rosetta (x86_64 仿真)"; /* No comment provided by engineer. */ "Enable Sound" = "啟用聲音"; @@ -1768,7 +1768,7 @@ "Engine" = "引擎"; /* No comment provided by engineer. */ -"Export all arguments as a text file. This is only for debugging purposes as UTM's built-in QEMU differs from upstream QEMU in supported arguments." = "將所有參數輸出為一個文字文件。這僅用於除錯目的,因為 UTM 的內建 QEMU 在支援的參數上與上游的 QEMU 不同。"; +"Export all arguments as a text file. This is only for debugging purposes as UTM's built-in QEMU differs from upstream QEMU in supported arguments." = "將所有引數輸出為一個文字文件。這只限用作除錯用途,因為 UTM 的內置 QEMU 在支援的引數上與上游的 QEMU 不同。"; /* No comment provided by engineer. */ "Export Debug Log" = "輸出除錯記錄"; @@ -1777,7 +1777,7 @@ "External Drive" = "外部磁碟"; /* UTMData */ -"Failed to parse download URL." = "無法解析已經下載的 URL。"; +"Failed to parse download URL." = "無法解析已下載的 URL。"; /* No comment provided by engineer. */ "Fetch latest Windows installer…" = "取得最新的 Windows 安裝工具⋯"; @@ -1792,7 +1792,7 @@ "Force Enable CPU Flags" = "強制啟用 CPU 標記"; /* No comment provided by engineer. */ -"Force multicore may improve speed of emulation but also might result in unstable and incorrect emulation." = "強行多核心可能會提高仿真速度,但也會導致不穩定與錯誤的仿真。"; +"Force multicore may improve speed of emulation but also might result in unstable and incorrect emulation." = "強行多核心可能會提高仿真速度,但亦會導致不穩定與不正確的仿真。"; /* No comment provided by engineer. */ "Force PS/2 controller" = "強行使用 PS/2 控制器"; @@ -1807,19 +1807,19 @@ "GPU Acceleration Supported" = "支援 GPU 加速"; /* No comment provided by engineer. */ -"Guest Address" = "客戶端位址"; +"Guest Address" = "客户端位址"; /* No comment provided by engineer. */ -"Guest Network" = "客戶端網絡"; +"Guest Network" = "客户端網絡"; /* No comment provided by engineer. */ -"Guest Network (IPv6)" = "客戶端網絡 (IPv6)"; +"Guest Network (IPv6)" = "客户端網絡 (IPv6)"; /* No comment provided by engineer. */ -"Guest Port" = "客戶端埠"; +"Guest Port" = "客户端埠"; /* No comment provided by engineer. */ -"Hardware interface on the guest used to mount this image. Different operating systems support different interfaces. The default will be the most common interface." = "用作裝載這個映像的客戶端硬體介面。不同的作業系統支援不同的介面。預設將會設定為最常見的介面。"; +"Hardware interface on the guest used to mount this image. Different operating systems support different interfaces. The default will be the most common interface." = "用作裝載這個映像的客户端硬體介面。不同的作業系統支援不同的介面。預設值將會設定為最常見的介面。"; /* No comment provided by engineer. */ "Hardware OpenGL Acceleration" = "硬體 OpenGL 加速"; @@ -1843,62 +1843,62 @@ "Host Port" = "主機埠"; /* No comment provided by engineer. */ -"If checked, no drive image will be stored with the VM. Instead you can mount/unmount image while the VM is running." = "如選取,將不會儲存磁碟映像檔至虛擬電腦內。然而,你可以於虛擬電腦執行時裝載/卸除安裝映像。"; +"If checked, no drive image will be stored with the VM. Instead you can mount/unmount image while the VM is running." = "如選擇,將不會儲存磁碟映像檔至虛擬機器當中。然而,你可以在虛擬機器執行時裝載/卸除安裝映像。"; /* No comment provided by engineer. */ -"If checked, the CPU flag will be enabled. Otherwise, the default value will be used." = "如選取,將會啟用這個 CPU 標記。否則將會使用預設值。"; +"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." = "如選取,磁碟映像檔將會與虛擬電腦一齊儲存。"; +"If checked, the drive image will be stored with the VM." = "如選擇,磁碟映像檔將會與虛擬機器一齊儲存。"; /* No comment provided by engineer. */ -"If checked, use local time for RTC which is required for Windows. Otherwise, use UTC clock." = "如選取,將會使用 Windows 所需要的 RTC 本地時間。否則將會使用 UTC 時鐘。"; +"If checked, use local time for RTC which is required for Windows. Otherwise, use UTC clock." = "如選擇,將會使用 Windows 所需要的 RTC 本地時間。否則將會使用 UTC 時鐘。"; /* 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 (⌃ + ⌥)。"; /* 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。"; +"If enabled, a virtiofs share tagged 'rosetta' will be available on the Linux guest for installing Rosetta for emulating x86_64 on ARM64." = "如啟用,標記為「rosetta」的 virtiofs 分享將會在 Linux 客户端上可用,用作安裝 Rosetta,可以在 arm64 上仿真 x86_64。"; /* No comment provided by engineer. */ -"If enabled, any existing screenshot will be deleted the next time the VM is started." = "如啟用,下次啟動虛擬電腦時,任何現存的螢幕截圖將會被刪除。"; +"If enabled, any existing screenshot will be deleted the next time the VM is started." = "如啟用,下次啟動虛擬機器時,任何現有的螢幕截圖將會被刪除。"; /* No comment provided by engineer. */ "If enabled, caps lock will be handled like other keys. If disabled, it is treated as a toggle that is synchronized with the host." = "如啟用,Caps Lock 將會同其他鍵一樣處理。如停用,它將會被視為開關鍵,並與主機同步。"; /* No comment provided by engineer. */ -"If enabled, input capture will toggle automatically when entering and exiting full screen mode." = "如啟用,於進入和離開全螢幕模式時,將會自動切換輸入擷取。"; +"If enabled, input capture will toggle automatically when entering and exiting full screen mode." = "如啟用,在進入和離開全螢幕模式時,將會自動切換輸入擷取。"; /* No comment provided by engineer. */ -"If enabled, input capture will toggle automatically when the VM's window is focused." = "如啟用,於虛擬電腦聚焦視窗時,將自動切換輸入擷取。"; +"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 鍵將會以系統預設工作(例如輸入國際文字)。"; +"If enabled, Option will be mapped to the Meta key which can be useful for emacs. Otherwise, option will work as the system intended (such as for entering international text)." = "如啟用,Option 鍵將會對應至 Meta 鍵,這對於 Emacs 很有用。否則,Option 鍵將會以系統預設工作 (例如輸入國際文字)。"; /* No comment provided by engineer. */ -"If enabled, resizing of the VM window will not be allowed." = "如啟用,將不會允許調整虛擬電腦視窗的大小。"; +"If enabled, resizing of the VM window will not be allowed." = "如啟用,將不會允許調整虛擬機器視窗的大小。"; /* No comment provided by engineer. */ "If enabled, scroll wheel input will be inverted." = "如啟用,將會反轉滾輪輸入。"; /* No comment provided by engineer. */ -"If enabled, the default input devices will be emulated on the USB bus." = "如啟用,預設輸入裝置將會於 USB 匯流排上仿真。"; +"If enabled, the default input devices will be emulated on the USB bus." = "如啟用,預設輸入裝置將會在 USB 匯流排上仿真。"; /* No comment provided by engineer. */ "If set, a frame limit can improve smoothness in rendering by preventing stutters when set to the lowest value your device can handle." = "如設定了幀限制,則當設定為你的裝置可以處理的最低值時,幀限制可以防止卡頓,藉此提升渲染的平滑度。"; /* No comment provided by engineer. */ -"If set, boot directly from a raw kernel image and initrd. Otherwise, boot from a supported ISO." = "如設定,直接由 Raw 核心映像檔與 initrd 啟動。否則由受支援的 ISO 啟動。"; +"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" = "映像檔種類"; @@ -1922,10 +1922,10 @@ "Install drivers and SPICE tools" = "安裝驅動程式與 SPICE 工具"; /* No comment provided by engineer. */ -"Install Windows 10 or higher" = "安裝 Windows 10 或者更高版本"; +"Install Windows 10 or higher" = "安裝 Windows 10 或更高版本"; /* No comment provided by engineer. */ -"Installation Instructions" = "安裝指引"; +"Installation Instructions" = "安裝指南"; /* No comment provided by engineer. */ "Instantiate PS/2 controller even when USB input is supported. Required for older Windows." = "即便支援 USB 輸入,仍然例項化 PS/2 控制器。這個功能對於舊版 Windows 為必需。"; @@ -1967,7 +1967,7 @@ "Mode" = "模式"; /* No comment provided by engineer. */ -"Modify settings for this VM." = "修改這個虛擬電腦的設定。"; +"Modify settings for this VM." = "修改這個虛擬機器的設定。"; /* UTMAppleConfigurationDevices */ "Mouse" = "滑鼠"; @@ -1979,10 +1979,10 @@ "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." = "將這個虛擬電腦由內部儲存空間移動至其他地方。"; +"Move this VM from internal storage to elsewhere." = "將這個虛擬機器從內部儲存空間移動至其他地方。"; /* No comment provided by engineer. */ "Network" = "網絡"; @@ -1994,37 +1994,37 @@ "New Drive…" = "新增磁碟機⋯"; /* No comment provided by engineer. */ -"New from template…" = "由這個範本新增⋯"; +"New from template…" = "從這個範本新增⋯"; /* No comment provided by engineer. */ "New Shared Directory…" = "新增分享目錄⋯"; /* No comment provided by engineer. */ -"New VM" = "新增虛擬電腦"; +"New VM" = "新增虛擬機器"; /* No comment provided by engineer. */ -"Older versions of UTM added each IDE device to a separate bus. Check this to change the configuration to place two units on each bus." = "舊版本的 UTM 將每個 IDE 裝置加至單獨的匯流排中。檢查這個項目以更改配置,以便於每個匯流排上放置兩個單元。"; +"Older versions of UTM added each IDE device to a separate bus. Check this to change the configuration to place two units on each bus." = "舊版本的 UTM 將每個 IDE 裝置加至單獨的匯流排當中。檢查這個項目以更改配置,以便於每個匯流排上放置兩個單元。"; /* No comment provided by engineer. */ "One Time Donation" = "一次捐贈"; /* No comment provided by engineer. */ -"Only available if host architecture matches the target. Otherwise, TCG emulation is used." = "僅當主機架構與目標匹配時才可用。否則,將使用 TCG 仿真。"; +"Only available if host architecture matches the target. Otherwise, TCG emulation is used." = "只限主機架構與目標匹配時可用。否則,將使用 TCG 仿真。"; /* No comment provided by engineer. */ -"Only available on macOS virtual machines." = "僅可用於 macOS 虛擬電腦。"; +"Only available on macOS virtual machines." = "只限 macOS 虛擬機器可用。"; /* No comment provided by engineer. */ -"Only available when Hypervisor is used on supported hardware. TSO speeds up Intel emulation in the guest at the cost of decreased performance in general." = "僅當於受支援的硬體上使用 Hypervisor 時才可用。TSO 提升了客戶端的 Intel 仿真速度,但以總體的效能降低為代價。"; +"Only available when Hypervisor is used on supported hardware. TSO speeds up Intel emulation in the guest at the cost of decreased performance in general." = "只限在受支援的硬體上使用 Hypervisor 時可用。TSO 提升了客户端的 Intel 仿真速度,但以總體的效能降低為代價。"; /* No comment provided by engineer. */ -"Open VM Settings" = "開啟虛擬電腦設定"; +"Open VM Settings" = "開啟虛擬機器設定"; /* No comment provided by engineer. */ -"Optionally select a directory to make accessible inside the VM. Note that support for shared directories varies by the guest operating system and may require additional guest drivers to be installed. See UTM support pages for more details." = "(可選) 選取一個目錄,使得可以在虛擬電腦內取用。請緊記,分享目錄的支援視乎客戶端作業系統而定,可能需要安裝額外的客戶端驅動程式。有關更多詳細訊息,請見 UTM 支援頁面。"; +"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" = "路徑"; @@ -2063,16 +2063,16 @@ "Read Only?" = "唯讀?"; /* No comment provided by engineer. */ -"Reclaim disk space by re-converting the disk image." = "透過重新轉換來回收磁碟空間。"; +"Reclaim disk space by re-converting the disk image." = "透過重新轉換以回收磁碟空間。"; /* No comment provided by engineer. */ "Reclaim Space" = "釋放空間"; /* No comment provided by engineer. */ -"Reject unknown connections by default" = "於預設情況下拒絕不明連線"; +"Reject unknown connections by default" = "根據預設拒絕不明連線"; /* No comment provided by engineer. */ -"Remove selected shortcut" = "移除已選取的捷徑"; +"Remove selected shortcut" = "移除已選擇的捷徑"; /* No comment provided by engineer. */ "Renderer Backend" = "渲染器後端"; @@ -2084,7 +2084,7 @@ "Requires restarting UTM to take affect." = "需要重新開啟 UTM 以生效。"; /* No comment provided by engineer. */ -"Requires SPICE guest agent tools to be installed." = "需要安裝 SPICE 客戶端代理程式工具。"; +"Requires SPICE guest agent tools to be installed." = "需要安裝 SPICE 客户端代理程式工具。"; /* No comment provided by engineer. */ "Reset UEFI Variables" = "重設 UEFI 變數"; @@ -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 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 %lld GiB?" = "調整大小為實驗性功能,可能會導致資料遺失。強烈建議你在繼續之前備份這個虛擬機器。你要將大小調整為 %lld GB 嗎?"; /* No comment provided by engineer. */ "Resolution" = "解像度"; @@ -2108,10 +2108,10 @@ "Resume" = "繼續"; /* No comment provided by engineer. */ -"Resume running VM." = "繼續正在執行的虛擬電腦。"; +"Resume running VM." = "繼續正在執行的虛擬機器。"; /* No comment provided by engineer. */ -"Reveal where the VM is stored." = "顯示虛擬電腦的儲存位置。"; +"Reveal where the VM is stored." = "顯示虛擬機器的儲存位置。"; /* No comment provided by engineer. */ "RNG Device" = "RNG 裝置"; @@ -2126,13 +2126,13 @@ "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." = "在螢幕前執行虛擬電腦。"; +"Run the VM in the foreground." = "在螢幕前執行虛擬機器。"; /* No comment provided by engineer. */ -"Run the VM in the foreground, without saving data changes to disk." = "在螢幕前執行虛擬電腦,但無需將資料更改儲存到磁碟。"; +"Run the VM in the foreground, without saving data changes to disk." = "在螢幕前執行虛擬機器,無需將資料更改儲存到磁碟。"; /* No comment provided by engineer. */ "Run without saving changes" = "執行但不儲存更改"; @@ -2141,10 +2141,10 @@ "Section" = "區域"; /* No comment provided by engineer. */ -"Secure Boot with TPM 2.0" = "使用 TPM 2.0 的保安啟動"; +"Secure Boot with TPM 2.0" = "採用 TPM 2.0 的保安啟動"; /* No comment provided by engineer. */ -"Select an existing disk image." = "選取一個現存的磁碟映像。"; +"Select an existing disk image." = "選擇一個現有的磁碟映像。"; /* No comment provided by engineer. */ "Serial" = "序列"; @@ -2162,7 +2162,7 @@ "Share…" = "分享⋯"; /* No comment provided by engineer. */ -"Share a copy of this VM and all its data." = "分享這個虛擬電腦與其所有資料的複製。"; +"Share a copy of this VM and all its data." = "分享這個虛擬機器與其所有資料的複製。"; /* No comment provided by engineer. */ "Share Directory" = "分享目錄"; @@ -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" = "分享目錄路徑"; @@ -2183,10 +2183,10 @@ "Shared Path" = "分享路徑"; /* No comment provided by engineer. */ -"Should be off for older operating systems such as Windows 7 or lower." = "對於較舊的作業系統應關閉,例如 Windows 7 或者更低版本。"; +"Should be off for older operating systems such as Windows 7 or lower." = "對於較舊的作業系統應關閉,例如 Windows 7 或更低版本。"; /* No comment provided by engineer. */ -"Should be on always unless the guest cannot boot because of this." = "除非客戶端因為這個設定而無法啟動,否則應始終開啟。"; +"Should be on always unless the guest cannot boot because of this." = "除非客户端因為這個設定而無法啟動,否則應始終開啟。"; /* No comment provided by engineer. */ "Show all devices…" = "顯示所有裝置⋯"; @@ -2225,10 +2225,10 @@ "Status" = "狀態"; /* No comment provided by engineer. */ -"Stop selected VM" = "停止已選取的虛擬電腦"; +"Stop selected VM" = "停止已選擇的虛擬機器"; /* No comment provided by engineer. */ -"Stop the running VM." = "停止正在執行的虛擬電腦。"; +"Stop the running VM." = "停止正在執行的虛擬機器。"; /* No comment provided by engineer. */ "Storage" = "儲存空間"; @@ -2243,7 +2243,7 @@ "Target" = "目標"; /* No comment provided by engineer. */ -"Terminate UTM and stop all running VMs." = "終止 UTM 並停止所有正在執行的虛擬電腦。"; +"Terminate UTM and stop all running VMs." = "終止 UTM 並停止所有正在執行的虛擬機器。"; /* No comment provided by engineer. */ "Text" = "文字"; @@ -2252,7 +2252,7 @@ "Text Color" = "文字顏色"; /* No comment provided by engineer. */ -"The amount of storage to allocate for this image. Ignored if importing an image. If this is a raw image, then an empty file of this size will be stored with the VM. Otherwise, the disk image will dynamically expand up to this size." = "為這個映像檔分配的儲存量。於輸入映像檔時會忽略這個參數。如果這是一個 Raw 映像檔,則這個大小的空檔案將會與虛擬電腦一齊儲存。否則,磁碟映像檔將會動態擴充至這個大小。"; +"The amount of storage to allocate for this image. Ignored if importing an image. If this is a raw image, then an empty file of this size will be stored with the VM. Otherwise, the disk image will dynamically expand up to this size." = "為這個映像檔分配的儲存量。在輸入映像檔時會忽略這個引數。如果這是一個 Raw 映像檔,則這個大小的空檔案將會與虛擬機器一齊儲存。否則,磁碟映像檔將會動態擴充至這個大小。"; /* No comment provided by engineer. */ "Theme" = "主題"; @@ -2264,19 +2264,19 @@ "These are advanced settings affecting QEMU which should be kept default unless you are running into issues." = "這些為影響 QEMU 的進階設定,除非遇到問題,否則你應當保持預設值。"; /* No comment provided by engineer. */ -"This is appended to the -machine argument." = "這會加至 -machine 參數的尾端。"; +"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: %@" = "必須使用 Finder 開啟這個虛擬電腦,將其重新加至 UTM 中。你可以於這個路徑中找到它:%@"; +"This virtual machine must be re-added to UTM by opening it with Finder. You can find it at the path: %@" = "必須使用 Finder 開啟這個虛擬機器,將其重新加至 UTM 中。你可以在這個路徑當中找到它:%@"; /* No comment provided by engineer. */ "TPM 2.0 Device" = "TPM 2.0 裝置"; /* No comment provided by engineer. */ -"TPM can be used to protect secrets in the guest operating system. Note that the host will always be able to read these secrets and therefore no expectation of physical security is provided." = "TPM 可以用來保護客戶端作業系統中的私密資料。請緊記,主機將始終可以讀取這些私密資料,所以無法提供預期的物理保安性。"; +"TPM can be used to protect secrets in the guest operating system. Note that the host will always be able to read these secrets and therefore no expectation of physical security is provided." = "TPM 可以用來保護客户端作業系統中的私密資料。請緊記,主機將始終可以讀取這些私密資料,所以無法提供預期的物理保安性。"; /* UTMAppleConfigurationDevices */ "Trackpad" = "觸控板"; @@ -2285,7 +2285,7 @@ "Tweaks" = "調整"; /* No comment provided by engineer. */ -"Ubuntu Install Guide" = "Ubuntu 安裝指引"; +"Ubuntu Install Guide" = "Ubuntu 安裝指南"; /* No comment provided by engineer. */ "UEFI Boot" = "UEFI 啟動"; @@ -2334,7 +2334,7 @@ "Virtualization Engine" = "虛擬化引擎"; /* No comment provided by engineer. */ -"VM display size is fixed" = "虛擬電腦顯示大小固定"; +"VM display size is fixed" = "固定虛擬機器顯示大小"; /* No comment provided by engineer. */ "Wait for Connection" = "等待連線"; @@ -2346,10 +2346,10 @@ "Width" = "寬度"; /* No comment provided by engineer. */ -"Windows Install Guide" = "Windows 安裝指引"; +"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" = "縮放"; diff --git a/Platform/zh-Hans.lproj/Localizable.strings b/Platform/zh-Hans.lproj/Localizable.strings index d1add4a08..a9c7ba605 100644 --- a/Platform/zh-Hans.lproj/Localizable.strings +++ b/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" = "缩放"; diff --git a/QEMUHelper/zh-Hans.lproj/InfoPlist.strings b/QEMUHelper/zh-Hans.lproj/InfoPlist.strings index 7d3ed8085..23503144f 100644 --- a/QEMUHelper/zh-Hans.lproj/InfoPlist.strings +++ b/QEMUHelper/zh-Hans.lproj/InfoPlist.strings @@ -5,5 +5,5 @@ "CFBundleName" = "QEMUHelper"; /* Copyright (human-readable) */ -"NSHumanReadableCopyright" = "版权所有 © 2020 osy。保留所有权利。"; +"NSHumanReadableCopyright" = "Copyright © 2020 osy. 保留所有权利。"; diff --git a/README.zh-Hans.md b/README.zh-Hans.md index 3e1452167..c1ebab216 100644 --- a/README.zh-Hans.md +++ b/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 的附加功能 diff --git a/Scripting/UTM.sdef b/Scripting/UTM.sdef index 80000bb73..a9c4e3466 100644 --- a/Scripting/UTM.sdef +++ b/Scripting/UTM.sdef @@ -103,6 +103,15 @@ + + + + + + + + + diff --git a/Scripting/UTMScripting.swift b/Scripting/UTMScripting.swift index 9cdec6d0f..2df8626d0 100644 --- a/Scripting/UTMScripting.swift +++ b/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). diff --git a/Scripting/UTMScriptingExportCommand.swift b/Scripting/UTMScriptingExportCommand.swift new file mode 100644 index 000000000..7704161b5 --- /dev/null +++ b/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() + } + } +} diff --git a/Scripting/UTMScriptingVirtualMachineImpl.swift b/Scripting/UTMScriptingVirtualMachineImpl.swift index f3b8b99d6..51302848f 100644 --- a/Scripting/UTMScriptingVirtualMachineImpl.swift +++ b/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 diff --git a/Services/UTMAppleVirtualMachine.swift b/Services/UTMAppleVirtualMachine.swift index 71ba4b53a..df3153c54 100644 --- a/Services/UTMAppleVirtualMachine.swift +++ b/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) 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) 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) 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 } } diff --git a/Services/UTMRegistryEntry.swift b/Services/UTMRegistryEntry.swift index fa812b909..73d21f7c9 100644 --- a/Services/UTMRegistryEntry.swift +++ b/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) + } + } } diff --git a/UTM.xcodeproj/project.pbxproj b/UTM.xcodeproj/project.pbxproj index f1bb27e1b..58b897f4b 100644 --- a/UTM.xcodeproj/project.pbxproj +++ b/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 = ""; }; C8958B6D243634DA002D86B4 /* ko */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ko; path = ko.lproj/Localizable.strings; sourceTree = ""; }; CD77BE432CB38F060074ADD2 /* UTMScriptingImportCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMScriptingImportCommand.swift; sourceTree = ""; }; + CD77BE412CAB519F0074ADD2 /* UTMScriptingExportCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMScriptingExportCommand.swift; sourceTree = ""; }; CE020BA224AEDC7C00B44AB6 /* UTMData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMData.swift; sourceTree = ""; }; CE020BAA24AEE00000B44AB6 /* UTMLoggingSwift.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMLoggingSwift.swift; sourceTree = ""; }; CE020BB524B14F8400B44AB6 /* UTMVirtualMachine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMVirtualMachine.swift; sourceTree = ""; }; @@ -1776,7 +1791,7 @@ CE03D0D324DCF6DD00F76B84 /* VMMetalViewInputDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VMMetalViewInputDelegate.swift; sourceTree = ""; }; CE056CA4242454100004B68A /* VMDisplayMetalViewController+Touch.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "VMDisplayMetalViewController+Touch.h"; sourceTree = ""; }; CE056CA5242454100004B68A /* VMDisplayMetalViewController+Touch.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "VMDisplayMetalViewController+Touch.m"; sourceTree = ""; }; - 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 = ""; }; + 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 = ""; }; CE059DC3243BFA3200338317 /* UTMLegacyQemuConfiguration+Sharing.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UTMLegacyQemuConfiguration+Sharing.h"; sourceTree = ""; }; CE059DC4243BFA3200338317 /* UTMLegacyQemuConfiguration+Sharing.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UTMLegacyQemuConfiguration+Sharing.m"; sourceTree = ""; }; CE059DC6243E9E3400338317 /* UTMLocationManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = UTMLocationManager.h; sourceTree = ""; }; @@ -2017,6 +2032,7 @@ CECF02562B706ADD00409FC0 /* UTMRemoteConnectInterface.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = UTMRemoteConnectInterface.h; sourceTree = ""; }; CECF02572B70909900409FC0 /* Info-Remote.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "Info-Remote.plist"; sourceTree = ""; }; CED234EC254796E500ED0A57 /* NumberTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NumberTextField.swift; sourceTree = ""; }; + 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 = ""; }; CED779E42C78C82A00EB82AE /* UTMTips.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMTips.swift; sourceTree = ""; }; CED779E92C7938D500EB82AE /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/Localizable.strings; sourceTree = ""; }; CED814E824C79F070042F0F1 /* VMConfigDriveCreateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VMConfigDriveCreateView.swift; sourceTree = ""; }; @@ -2041,6 +2057,7 @@ CEEB66452284B942002737B2 /* VMKeyboardButton.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = VMKeyboardButton.m; sourceTree = ""; }; CEEC811A24E48EC600ACB0B3 /* SettingsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; CEECE13B25E47D9500A2AAB8 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + CEEF26A62CEDAEEA003F7B8C /* UTMDownloadMacSupportToolsTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMDownloadMacSupportToolsTask.swift; sourceTree = ""; }; CEF01DB12B6724A300725A0F /* UTMSpiceVirtualMachine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMSpiceVirtualMachine.swift; sourceTree = ""; }; CEF01DB62B674BF000725A0F /* UTMPipeInterface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMPipeInterface.swift; sourceTree = ""; }; CEF0300526A25A6900667B63 /* VMWizardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VMWizardView.swift; sourceTree = ""; }; @@ -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 = ""; @@ -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 */, diff --git a/patches/libsoup-2.74.2.patch b/patches/libsoup-2.74.2.patch deleted file mode 100644 index dae2f76ea..000000000 --- a/patches/libsoup-2.74.2.patch +++ /dev/null @@ -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 ', 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) - diff --git a/patches/libsoup-3.6.0.patch b/patches/libsoup-3.6.0.patch new file mode 100644 index 000000000..3a3858a41 --- /dev/null +++ b/patches/libsoup-3.6.0.patch @@ -0,0 +1,255 @@ +From 95102597efaddede487bd03c191fa0a08b70e3b6 Mon Sep 17 00:00:00 2001 +From: osy +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 ', 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 +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 ', 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 ', 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 + diff --git a/patches/phodav-2.5.patch b/patches/phodav-2.5.patch deleted file mode 100644 index 460a35399..000000000 --- a/patches/phodav-2.5.patch +++ /dev/null @@ -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) - diff --git a/patches/phodav-3.0.patch b/patches/phodav-3.0.patch new file mode 100644 index 000000000..ad9627ede --- /dev/null +++ b/patches/phodav-3.0.patch @@ -0,0 +1,27 @@ +From ddca2a3c7a5cabf19ae94e4a6482457cb5fa1b30 Mon Sep 17 00:00:00 2001 +From: osy +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 + diff --git a/patches/qemu-9.1.0-utm.patch b/patches/qemu-9.1.0-utm.patch index d32ad890d..835005e3c 100644 --- a/patches/qemu-9.1.0-utm.patch +++ b/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 +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 + diff --git a/patches/sources b/patches/sources index 69f89f75d..9497c75a7 100644 --- a/patches/sources +++ b/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" diff --git a/patches/spice-gtk-0.40.patch b/patches/spice-gtk-0.42.patch similarity index 65% rename from patches/spice-gtk-0.40.patch rename to patches/spice-gtk-0.42.patch index a38ecbfad..d5997f285 100644 --- a/patches/spice-gtk-0.40.patch +++ b/patches/spice-gtk-0.42.patch @@ -1,134 +1,7 @@ -From 2f16f6d4b0d6dde0d1d518f61c01f5f972caa008 Mon Sep 17 00:00:00 2001 -From: osy -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 -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 --#include - #include - - #include "continuation.h" - -+#ifdef HAVE_LIBUCONTEXT -+#include -+#define ucontext_t libucontext_ucontext_t -+#define getcontext libucontext_getcontext -+#define setcontext libucontext_setcontext -+#define swapcontext libucontext_swapcontext -+#define makecontext libucontext_makecontext -+#else -+#include -+#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 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 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 -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 -+#ifdef HAVE_PHYSICAL_CD - #include - #include -+#endif // HAVE_PHYSICAL_CD - #else - #include - #include -+#ifdef HAVE_PHYSICAL_CD - #ifdef __APPLE__ - #include - #include -@@ -44,6 +47,7 @@ - #include - #include - #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 -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 -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 -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 -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 ---- - 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 -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 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 diff --git a/scripts/build_dependencies.sh b/scripts/build_dependencies.sh index 6732e6885..01b96eda7 100755 --- a/scripts/build_dependencies.sh +++ b/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 () { diff --git a/utmctl/UTMCtl.swift b/utmctl/UTMCtl.swift index 2b78dd723..4f48a7ea3 100644 --- a/utmctl/UTMCtl.swift +++ b/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( diff --git a/utmctl/utmctl-unsigned.entitlements b/utmctl/utmctl-unsigned.entitlements index 97d410104..65ed91648 100644 --- a/utmctl/utmctl-unsigned.entitlements +++ b/utmctl/utmctl-unsigned.entitlements @@ -3,7 +3,7 @@ com.apple.security.app-sandbox - + com.apple.security.network.client com.apple.security.scripting-targets