VMConfigAppleBootView.swift 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184
  1. //
  2. // Copyright © 2021 osy. All rights reserved.
  3. //
  4. // Licensed under the Apache License, Version 2.0 (the "License");
  5. // you may not use this file except in compliance with the License.
  6. // You may obtain a copy of the License at
  7. //
  8. // http://www.apache.org/licenses/LICENSE-2.0
  9. //
  10. // Unless required by applicable law or agreed to in writing, software
  11. // distributed under the License is distributed on an "AS IS" BASIS,
  12. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. // See the License for the specific language governing permissions and
  14. // limitations under the License.
  15. //
  16. import Virtualization
  17. import SwiftUI
  18. @available(macOS 11, *)
  19. struct VMConfigAppleBootView: View {
  20. private enum BootloaderSelection: Int, Identifiable {
  21. var id: Int {
  22. self.rawValue
  23. }
  24. case kernel
  25. case ramdisk
  26. case ipsw
  27. case unsupported
  28. }
  29. @Binding var config: UTMAppleConfigurationSystem
  30. @EnvironmentObject private var data: UTMData
  31. @State private var operatingSystem: UTMAppleConfigurationBoot.OperatingSystem = .none
  32. @State private var alertBootloaderSelection: BootloaderSelection?
  33. @State private var importBootloaderSelection: BootloaderSelection?
  34. @State private var importFileShown: Bool = false
  35. private var currentOperatingSystem: UTMAppleConfigurationBoot.OperatingSystem {
  36. config.boot.operatingSystem
  37. }
  38. var body: some View {
  39. Form {
  40. VMConfigConstantPicker("Operating System", selection: $operatingSystem)
  41. Picker("Bootloader", selection: $config.boot.hasUefiBoot) {
  42. Text(operatingSystem.prettyValue).tag(false)
  43. if #available(macOS 13, *) {
  44. Text("UEFI").tag(true)
  45. }
  46. }
  47. .onAppear {
  48. operatingSystem = currentOperatingSystem
  49. }
  50. .onChange(of: operatingSystem) { newValue in
  51. guard newValue != currentOperatingSystem else {
  52. return
  53. }
  54. if newValue == .linux {
  55. if #available(macOS 13, *) {
  56. config.boot.hasUefiBoot = true
  57. config.boot.operatingSystem = .linux
  58. } else {
  59. alertBootloaderSelection = .kernel
  60. }
  61. } else if newValue == .macOS {
  62. if #available(macOS 12, *) {
  63. alertBootloaderSelection = .ipsw
  64. } else {
  65. alertBootloaderSelection = .unsupported
  66. }
  67. } else {
  68. config.boot.operatingSystem = .none
  69. }
  70. // don't change display until AFTER file selected
  71. importBootloaderSelection = nil
  72. operatingSystem = currentOperatingSystem
  73. }.onChange(of: config.boot.hasUefiBoot) { newValue in
  74. if !newValue && operatingSystem == .linux && config.boot.linuxKernelURL == nil {
  75. alertBootloaderSelection = .kernel
  76. operatingSystem = .none
  77. } else if newValue {
  78. config.genericPlatform = UTMAppleConfigurationGenericPlatform()
  79. }
  80. }.alert(item: $alertBootloaderSelection) { selection in
  81. let okay = Alert.Button.default(Text("OK")) {
  82. importBootloaderSelection = selection
  83. importFileShown = true
  84. }
  85. switch selection {
  86. case .kernel:
  87. return Alert(title: Text("Please select an uncompressed Linux kernel image."), dismissButton: okay)
  88. case .ipsw:
  89. return Alert(title: Text("Please select a macOS recovery IPSW."), primaryButton: okay, secondaryButton: .cancel())
  90. case .unsupported:
  91. return Alert(title: Text("This operating system is unsupported on your machine."))
  92. default:
  93. return Alert(title: Text("Select a file."), dismissButton: okay)
  94. }
  95. }.fileImporter(isPresented: $importFileShown,
  96. allowedContentTypes: [importBootloaderSelection == .ipsw ? .ipsw : .data],
  97. onCompletion: selectImportedFile)
  98. if operatingSystem == .linux && !config.boot.hasUefiBoot {
  99. Section(header: Text("Linux Settings")) {
  100. FileBrowseField("Kernel Image", url: $config.boot.linuxKernelURL, isFileImporterPresented: $importFileShown, hasClearButton: false) {
  101. importBootloaderSelection = .kernel
  102. }
  103. FileBrowseField("Ramdisk (optional)", url: $config.boot.linuxInitialRamdiskURL, isFileImporterPresented: $importFileShown) {
  104. importBootloaderSelection = .ramdisk
  105. }
  106. TextField("Boot arguments", text: $config.boot.linuxCommandLine.bound)
  107. }
  108. } else if #available(macOS 12, *), operatingSystem == .macOS {
  109. #if arch(arm64)
  110. Section(header: Text("macOS Settings")) {
  111. FileBrowseField("IPSW Install Image", url: $config.boot.macRecoveryIpswURL, isFileImporterPresented: $importFileShown) {
  112. importBootloaderSelection = .ipsw
  113. }
  114. }
  115. #endif
  116. }
  117. }.onAppear {
  118. operatingSystem = currentOperatingSystem
  119. }
  120. }
  121. private func selectImportedFile(result: Result<URL, Error>) {
  122. // reset operating system to old value
  123. guard let selection = importBootloaderSelection else {
  124. return
  125. }
  126. data.busyWorkAsync {
  127. let url = try result.get()
  128. switch selection {
  129. case .ipsw:
  130. if #available(macOS 12, *) {
  131. #if arch(arm64)
  132. let scopedAccess = url.startAccessingSecurityScopedResource()
  133. defer {
  134. if scopedAccess {
  135. url.stopAccessingSecurityScopedResource()
  136. }
  137. }
  138. let image = try await VZMacOSRestoreImage.image(from: url)
  139. guard let model = image.mostFeaturefulSupportedConfiguration?.hardwareModel else {
  140. throw NSLocalizedString("Your machine does not support running this IPSW.", comment: "VMConfigAppleBootView")
  141. }
  142. await MainActor.run {
  143. config.macPlatform = UTMAppleConfigurationMacPlatform(newHardware: model)
  144. config.boot.operatingSystem = .macOS
  145. config.boot.macRecoveryIpswURL = url
  146. }
  147. #endif
  148. }
  149. case .kernel:
  150. await MainActor.run {
  151. config.genericPlatform = UTMAppleConfigurationGenericPlatform()
  152. config.boot.operatingSystem = .linux
  153. config.boot.linuxKernelURL = url
  154. config.boot.hasUefiBoot = false
  155. }
  156. case .ramdisk:
  157. await MainActor.run {
  158. config.boot.linuxInitialRamdiskURL = url
  159. }
  160. case .unsupported:
  161. break
  162. }
  163. await MainActor.run {
  164. operatingSystem = currentOperatingSystem
  165. }
  166. }
  167. }
  168. }
  169. @available(macOS 12, *)
  170. struct VMConfigAppleBootView_Previews: PreviewProvider {
  171. @State static private var config = UTMAppleConfigurationSystem()
  172. static var previews: some View {
  173. VMConfigAppleBootView(config: $config)
  174. }
  175. }