123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220 |
- //
- // 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
- import Virtualization
- /// Device settings.
- @available(iOS, unavailable, message: "Apple Virtualization not available on iOS")
- @available(macOS 11, *)
- struct UTMAppleConfigurationVirtualization: Codable {
- enum PointerDevice: String, CaseIterable, QEMUConstant {
- case disabled = "Disabled"
- case mouse = "Mouse"
- case trackpad = "Trackpad"
-
- var prettyValue: String {
- switch self {
- case .disabled: return NSLocalizedString("Disabled", comment: "UTMAppleConfigurationDevices")
- case .mouse: return NSLocalizedString("Mouse", comment: "UTMAppleConfigurationDevices")
- case .trackpad: return NSLocalizedString("Trackpad", comment: "UTMAppleConfigurationDevices")
- }
- }
- }
-
- var hasAudio: Bool = false
-
- var hasBalloon: Bool = true
-
- var hasEntropy: Bool = true
-
- var hasKeyboard: Bool = false
-
- var hasPointer: Bool = false
-
- var hasTrackpad: Bool = false
-
- var hasRosetta: Bool?
-
- var hasClipboardSharing: Bool = false
-
- enum CodingKeys: String, CodingKey {
- case hasAudio = "Audio"
- case hasBalloon = "Balloon"
- case hasEntropy = "Entropy"
- case hasKeyboard = "Keyboard"
- case hasPointer = "Pointer"
- case hasTrackpad = "Trackpad"
- case rosetta = "Rosetta"
- case hasClipboardSharing = "ClipboardSharing"
- }
-
- init() {
- }
-
- init(from decoder: Decoder) throws {
- let values = try decoder.container(keyedBy: CodingKeys.self)
- hasAudio = try values.decode(Bool.self, forKey: .hasAudio)
- hasBalloon = try values.decode(Bool.self, forKey: .hasBalloon)
- hasEntropy = try values.decode(Bool.self, forKey: .hasEntropy)
- hasKeyboard = try values.decode(Bool.self, forKey: .hasKeyboard)
- if let legacyPointer = try? values.decode(PointerDevice.self, forKey: .hasPointer) {
- hasPointer = legacyPointer != .disabled
- hasTrackpad = legacyPointer == .trackpad
- } else {
- hasPointer = try values.decode(Bool.self, forKey: .hasPointer)
- hasTrackpad = try values.decodeIfPresent(Bool.self, forKey: .hasTrackpad) ?? false
- }
- if #available(macOS 13, *) {
- hasRosetta = try values.decodeIfPresent(Bool.self, forKey: .rosetta)
- hasClipboardSharing = try values.decodeIfPresent(Bool.self, forKey: .hasClipboardSharing) ?? false
- }
- }
-
- func encode(to encoder: Encoder) throws {
- var container = encoder.container(keyedBy: CodingKeys.self)
- try container.encode(hasAudio, forKey: .hasAudio)
- try container.encode(hasBalloon, forKey: .hasBalloon)
- try container.encode(hasEntropy, forKey: .hasEntropy)
- try container.encode(hasKeyboard, forKey: .hasKeyboard)
- try container.encode(hasPointer, forKey: .hasPointer)
- try container.encode(hasTrackpad, forKey: .hasTrackpad)
- try container.encodeIfPresent(hasRosetta, forKey: .rosetta)
- try container.encode(hasClipboardSharing, forKey: .hasClipboardSharing)
- }
- }
- // MARK: - Conversion of old config format
- @available(iOS, unavailable, message: "Apple Virtualization not available on iOS")
- @available(macOS 11, *)
- extension UTMAppleConfigurationVirtualization {
- init(migrating oldConfig: UTMLegacyAppleConfiguration) {
- self.init()
- hasBalloon = oldConfig.isBalloonEnabled
- hasEntropy = oldConfig.isEntropyEnabled
- if #available(macOS 12, *) {
- hasAudio = oldConfig.isAudioEnabled
- hasKeyboard = oldConfig.isKeyboardEnabled
- hasPointer = oldConfig.isPointingEnabled
- }
- }
- }
- // MARK: - Creating Apple config
- @available(iOS, unavailable, message: "Apple Virtualization not available on iOS")
- @available(macOS 11, *)
- extension UTMAppleConfigurationVirtualization {
- func fillVZConfiguration(_ vzconfig: VZVirtualMachineConfiguration, isMacOSGuest: Bool = false) throws {
- if hasBalloon && !isMacOSGuest {
- vzconfig.memoryBalloonDevices = [VZVirtioTraditionalMemoryBalloonDeviceConfiguration()]
- }
- if hasEntropy {
- vzconfig.entropyDevices = [VZVirtioEntropyDeviceConfiguration()]
- }
- if #available(macOS 12, *) {
- if hasAudio {
- let audioInputConfiguration = VZVirtioSoundDeviceConfiguration()
- let audioInput = VZVirtioSoundDeviceInputStreamConfiguration()
- audioInput.source = VZHostAudioInputStreamSource()
- audioInputConfiguration.streams = [audioInput]
- let audioOutputConfiguration = VZVirtioSoundDeviceConfiguration()
- let audioOutput = VZVirtioSoundDeviceOutputStreamConfiguration()
- audioOutput.sink = VZHostAudioOutputStreamSink()
- audioOutputConfiguration.streams = [audioOutput]
- vzconfig.audioDevices = [audioInputConfiguration, audioOutputConfiguration]
- }
- if hasKeyboard {
- vzconfig.keyboards = [VZUSBKeyboardConfiguration()]
- #if arch(arm64)
- if #available(macOS 14, *), isMacOSGuest {
- vzconfig.keyboards = [VZMacKeyboardConfiguration()]
- }
- #endif
- }
- if hasPointer {
- vzconfig.pointingDevices = [VZUSBScreenCoordinatePointingDeviceConfiguration()]
- #if arch(arm64)
- if #available(macOS 13, *), isMacOSGuest && hasTrackpad {
- // replace with trackpad device
- vzconfig.pointingDevices = [VZMacTrackpadConfiguration()]
- }
- #endif
- }
- } else {
- if hasAudio || hasKeyboard || hasPointer {
- throw UTMAppleConfigurationError.featureNotSupported
- }
- }
- if #available(macOS 13, *) {
- #if arch(arm64)
- if hasRosetta == true {
- let rosettaDirectoryShare = try VZLinuxRosettaDirectoryShare()
- if #available(macOS 14, *) {
- // enable cache if possible
- try? rosettaDirectoryShare.setCachingOptions(.defaultUnixSocket)
- }
- let fileSystemDevice = VZVirtioFileSystemDeviceConfiguration(tag: "rosetta")
- fileSystemDevice.share = rosettaDirectoryShare
- vzconfig.directorySharingDevices.append(fileSystemDevice)
- }
- #else
- if hasRosetta == true {
- throw UTMAppleConfigurationError.rosettaNotSupported
- }
- #endif
- if hasClipboardSharing && !isMacOSGuest {
- let spiceClipboardAgent = VZSpiceAgentPortAttachment()
- spiceClipboardAgent.sharesClipboard = true
- let consolePort = VZVirtioConsolePortConfiguration()
- consolePort.name = VZSpiceAgentPortAttachment.spiceAgentPortName
- consolePort.attachment = spiceClipboardAgent
- consolePort.isConsole = false
- let consoleDevice = VZVirtioConsoleDeviceConfiguration()
- consoleDevice.ports[0] = consolePort
- vzconfig.consoleDevices.append(consoleDevice)
- }
- } else {
- if hasRosetta == true || hasClipboardSharing {
- throw UTMAppleConfigurationError.featureNotSupported
- }
- }
- }
- }
- // MARK: prepare save
- extension UTMAppleConfigurationVirtualization {
- func prepareSave(for packageURL: URL) async throws {
- if #available(macOS 13, *), hasRosetta == true {
- try await installRosetta()
- }
- }
-
- @available(macOS 13, *)
- private func installRosetta() async throws {
- #if arch(arm64)
- let rosettaAvailability = VZLinuxRosettaDirectoryShare.availability
- if rosettaAvailability == .notSupported {
- throw UTMAppleConfigurationError.rosettaNotSupported
- } else if rosettaAvailability == .notInstalled {
- try await VZLinuxRosettaDirectoryShare.installRosetta()
- }
- #else
- throw UTMAppleConfigurationError.rosettaNotSupported
- #endif
- }
- }
|