371 lines
12 KiB
Swift
371 lines
12 KiB
Swift
//
|
|
// 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
|
|
|
|
/// Settings for a QEMU configuration
|
|
final class UTMQemuConfiguration: UTMConfiguration {
|
|
/// Basic information and icon
|
|
@Published var _information: UTMConfigurationInfo = .init()
|
|
|
|
/// System settings
|
|
@Published var _system: UTMQemuConfigurationSystem = .init()
|
|
|
|
/// Additional QEMU tweaks
|
|
@Published var _qemu: UTMQemuConfigurationQEMU = .init()
|
|
|
|
/// Input settings
|
|
@Published var _input: UTMQemuConfigurationInput = .init()
|
|
|
|
/// Sharing settings
|
|
@Published var _sharing: UTMQemuConfigurationSharing = .init()
|
|
|
|
/// All displays
|
|
@Published var _displays: [UTMQemuConfigurationDisplay] = []
|
|
|
|
/// All drives
|
|
@Published var _drives: [UTMQemuConfigurationDrive] = []
|
|
|
|
/// All network adapters
|
|
@Published var _networks: [UTMQemuConfigurationNetwork] = []
|
|
|
|
/// All serial ouputs
|
|
@Published var _serials: [UTMQemuConfigurationSerial] = []
|
|
|
|
/// All audio devices
|
|
@Published var _sound: [UTMQemuConfigurationSound] = []
|
|
|
|
/// True if configuration is migrated from a legacy config. Not saved.
|
|
private(set) var isLegacy: Bool = false
|
|
|
|
var backend: UTMBackend {
|
|
.qemu
|
|
}
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case information = "Information"
|
|
case system = "System"
|
|
case qemu = "QEMU"
|
|
case input = "Input"
|
|
case sharing = "Sharing"
|
|
case displays = "Display"
|
|
case drives = "Drive"
|
|
case networks = "Network"
|
|
case serials = "Serial"
|
|
case sound = "Sound"
|
|
case backend = "Backend"
|
|
case configurationVersion = "ConfigurationVersion"
|
|
}
|
|
|
|
init() {
|
|
reset()
|
|
}
|
|
|
|
required init(from decoder: Decoder) throws {
|
|
let values = try decoder.container(keyedBy: CodingKeys.self)
|
|
let backend = try values.decodeIfPresent(UTMBackend.self, forKey: .backend) ?? .qemu
|
|
guard backend == .qemu else {
|
|
throw UTMConfigurationError.invalidBackend
|
|
}
|
|
let version = try values.decodeIfPresent(Int.self, forKey: .configurationVersion) ?? 0
|
|
guard version >= Self.oldestVersion else {
|
|
throw UTMConfigurationError.versionTooLow
|
|
}
|
|
guard version <= Self.currentVersion else {
|
|
throw UTMConfigurationError.versionTooHigh
|
|
}
|
|
_information = try values.decode(UTMConfigurationInfo.self, forKey: .information)
|
|
_system = try values.decode(UTMQemuConfigurationSystem.self, forKey: .system)
|
|
_qemu = try values.decode(UTMQemuConfigurationQEMU.self, forKey: .qemu)
|
|
_input = try values.decode(UTMQemuConfigurationInput.self, forKey: .input)
|
|
_sharing = try values.decode(UTMQemuConfigurationSharing.self, forKey: .sharing)
|
|
_displays = try values.decode([UTMQemuConfigurationDisplay].self, forKey: .displays)
|
|
_drives = try values.decode([UTMQemuConfigurationDrive].self, forKey: .drives)
|
|
_networks = try values.decode([UTMQemuConfigurationNetwork].self, forKey: .networks)
|
|
_serials = try values.decode([UTMQemuConfigurationSerial].self, forKey: .serials)
|
|
_sound = try values.decode([UTMQemuConfigurationSound].self, forKey: .sound)
|
|
}
|
|
|
|
func encode(to encoder: Encoder) throws {
|
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
|
try container.encode(_information, forKey: .information)
|
|
try container.encode(_system, forKey: .system)
|
|
try container.encode(_qemu, forKey: .qemu)
|
|
try container.encode(_input, forKey: .input)
|
|
try container.encode(_sharing, forKey: .sharing)
|
|
try container.encode(_displays, forKey: .displays)
|
|
try container.encode(_drives, forKey: .drives)
|
|
try container.encode(_networks, forKey: .networks)
|
|
try container.encode(_serials, forKey: .serials)
|
|
try container.encode(_sound, forKey: .sound)
|
|
try container.encode(UTMBackend.qemu, forKey: .backend)
|
|
try container.encode(Self.currentVersion, forKey: .configurationVersion)
|
|
}
|
|
}
|
|
|
|
enum UTMQemuConfigurationError: Error {
|
|
case migrationFailed
|
|
case uefiNotSupported
|
|
}
|
|
|
|
extension UTMQemuConfigurationError: LocalizedError {
|
|
var errorDescription: String? {
|
|
switch self {
|
|
case .migrationFailed:
|
|
return NSLocalizedString("Failed to migrate configuration from a previous UTM version.", comment: "UTMQemuConfigurationError")
|
|
case .uefiNotSupported:
|
|
return NSLocalizedString("UEFI is not supported with this architecture.", comment: "UTMQemuConfigurationError")
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Public accessors
|
|
|
|
@MainActor extension UTMQemuConfiguration {
|
|
var information: UTMConfigurationInfo {
|
|
get {
|
|
_information
|
|
}
|
|
|
|
set {
|
|
_information = newValue
|
|
}
|
|
}
|
|
|
|
var system: UTMQemuConfigurationSystem {
|
|
get {
|
|
_system
|
|
}
|
|
|
|
set {
|
|
_system = newValue
|
|
}
|
|
}
|
|
|
|
var qemu: UTMQemuConfigurationQEMU {
|
|
get {
|
|
_qemu
|
|
}
|
|
|
|
set {
|
|
_qemu = newValue
|
|
}
|
|
}
|
|
|
|
var input: UTMQemuConfigurationInput {
|
|
get {
|
|
_input
|
|
}
|
|
|
|
set {
|
|
_input = newValue
|
|
}
|
|
}
|
|
|
|
var sharing: UTMQemuConfigurationSharing {
|
|
get {
|
|
_sharing
|
|
}
|
|
|
|
set {
|
|
_sharing = newValue
|
|
}
|
|
}
|
|
|
|
var displays: [UTMQemuConfigurationDisplay] {
|
|
get {
|
|
_displays
|
|
}
|
|
|
|
set {
|
|
_displays = newValue
|
|
}
|
|
}
|
|
|
|
var drives: [UTMQemuConfigurationDrive] {
|
|
get {
|
|
_drives
|
|
}
|
|
|
|
set {
|
|
_drives = newValue
|
|
}
|
|
}
|
|
|
|
var networks: [UTMQemuConfigurationNetwork] {
|
|
get {
|
|
_networks
|
|
}
|
|
|
|
set {
|
|
_networks = newValue
|
|
}
|
|
}
|
|
|
|
var serials: [UTMQemuConfigurationSerial] {
|
|
get {
|
|
_serials
|
|
}
|
|
|
|
set {
|
|
_serials = newValue
|
|
}
|
|
}
|
|
|
|
var sound: [UTMQemuConfigurationSound] {
|
|
get {
|
|
_sound
|
|
}
|
|
|
|
set {
|
|
_sound = newValue
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Defaults
|
|
|
|
extension UTMQemuConfiguration {
|
|
private func reset(all: Bool = true) {
|
|
if all {
|
|
_information = .init()
|
|
_system = .init()
|
|
_drives = []
|
|
}
|
|
_qemu = .init()
|
|
_input = .init()
|
|
_sharing = .init()
|
|
_displays = []
|
|
_networks = []
|
|
_serials = []
|
|
_sound = []
|
|
}
|
|
|
|
@MainActor func reset(forArchitecture architecture: QEMUArchitecture, target: any QEMUTarget) {
|
|
reset(all: false)
|
|
qemu = .init(forArchitecture: architecture, target: target)
|
|
input = .init(forArchitecture: architecture, target: target)
|
|
sharing = .init(forArchitecture: architecture, target: target)
|
|
system.cpu = architecture.cpuType.default
|
|
if let display = UTMQemuConfigurationDisplay(forArchitecture: architecture, target: target) {
|
|
displays = [display]
|
|
} else {
|
|
serials = [UTMQemuConfigurationSerial()]
|
|
}
|
|
if let network = UTMQemuConfigurationNetwork(forArchitecture: architecture, target: target) {
|
|
networks = [network]
|
|
}
|
|
if let _sound = UTMQemuConfigurationSound(forArchitecture: architecture, target: target) {
|
|
sound = [_sound]
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Conversion of old config format
|
|
|
|
extension UTMQemuConfiguration {
|
|
convenience init(migrating oldConfig: UTMLegacyQemuConfiguration) {
|
|
self.init()
|
|
isLegacy = true
|
|
_information = .init(migrating: oldConfig)
|
|
_system = .init(migrating: oldConfig)
|
|
_qemu = .init(migrating: oldConfig)
|
|
_input = .init(migrating: oldConfig)
|
|
_sharing = .init(migrating: oldConfig)
|
|
if let display = UTMQemuConfigurationDisplay(migrating: oldConfig) {
|
|
_displays = [display]
|
|
}
|
|
_drives = (0..<oldConfig.countDrives).map({ i in UTMQemuConfigurationDrive(migrating: oldConfig, at: i) })
|
|
// remove efi_vars which is no longer stored as a drive
|
|
_drives.removeAll { drive in
|
|
drive.imageName == QEMUPackageFileName.efiVariables.rawValue
|
|
}
|
|
if let network = UTMQemuConfigurationNetwork(migrating: oldConfig) {
|
|
_networks = [network]
|
|
}
|
|
if let serial = UTMQemuConfigurationSerial(migrating: oldConfig) {
|
|
_serials = [serial]
|
|
}
|
|
if let __sound = UTMQemuConfigurationSound(migrating: oldConfig) {
|
|
_sound = [__sound]
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Saving data
|
|
|
|
@MainActor extension UTMQemuConfiguration {
|
|
func prepareSave(for packageURL: URL) async throws {
|
|
guard isLegacy else {
|
|
return // nothing needed
|
|
}
|
|
// move Images to Data
|
|
let fileManager = FileManager.default
|
|
let imagesURL = packageURL.appendingPathComponent(QEMUPackageFileName.images.rawValue)
|
|
let dataURL = packageURL.appendingPathComponent(Self.dataDirectoryName)
|
|
if fileManager.fileExists(atPath: imagesURL.path) {
|
|
guard !fileManager.fileExists(atPath: dataURL.path) else {
|
|
throw UTMQemuConfigurationError.migrationFailed
|
|
}
|
|
try await Task.detached {
|
|
try FileManager.default.moveItem(at: imagesURL, to: dataURL)
|
|
}.value
|
|
}
|
|
// update any drives
|
|
for i in 0..<drives.count {
|
|
if !drives[i].isExternal, let oldImageURL = drives[i].imageURL {
|
|
drives[i].imageURL = dataURL.appendingPathComponent(oldImageURL.lastPathComponent)
|
|
}
|
|
}
|
|
// move icon
|
|
if information.isIconCustom, let oldIconURL = information.iconURL {
|
|
let newIconURL = dataURL.appendingPathComponent(oldIconURL.lastPathComponent)
|
|
try await Task.detached {
|
|
try FileManager.default.moveItem(at: oldIconURL, to: newIconURL)
|
|
}.value
|
|
information.iconURL = newIconURL
|
|
}
|
|
// move debug log
|
|
if let oldLogURL = qemu.debugLogURL, fileManager.fileExists(atPath: oldLogURL.path) {
|
|
let newLogURL = dataURL.appendingPathComponent(oldLogURL.lastPathComponent)
|
|
await Task.detached {
|
|
do {
|
|
try FileManager.default.moveItem(at: oldLogURL, to: newLogURL)
|
|
} catch {
|
|
// okay to fail
|
|
try? FileManager.default.removeItem(at: oldLogURL)
|
|
}
|
|
}.value
|
|
qemu.debugLogURL = newLogURL
|
|
}
|
|
// move efi variables
|
|
qemu.efiVarsURL = nil // will be set at saveData
|
|
}
|
|
|
|
func saveData(to dataURL: URL) async throws -> [URL] {
|
|
var existingDataURLs = [URL]()
|
|
|
|
existingDataURLs += try await _information.saveData(to: dataURL)
|
|
existingDataURLs += try await _qemu.saveData(to: dataURL, for: system)
|
|
|
|
for i in 0..<drives.count {
|
|
existingDataURLs += try await _drives[i].saveData(to: dataURL)
|
|
}
|
|
|
|
return existingDataURLs
|
|
}
|
|
}
|