Skip to content

Commit 5766c71

Browse files
committed
wizard: create classic Mac OS machine
1 parent 4964373 commit 5766c71

File tree

8 files changed

+225
-22
lines changed

8 files changed

+225
-22
lines changed

Icons/macos.png

3.64 KB
Loading

Platform/Shared/VMWizardHardwareView.swift

Lines changed: 112 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,71 @@ import Virtualization
2020
#endif
2121

2222
struct VMWizardHardwareView: View {
23+
private enum ClassicMacSystem: CaseIterable, Identifiable {
24+
case quadra800
25+
//case powerMacG3Beige
26+
case powerMacG4
27+
case powerMacG5
28+
29+
var id: Self { self }
30+
31+
var title: LocalizedStringKey {
32+
switch self {
33+
case .quadra800: "Macintosh Quadra 800 (M68K)"
34+
//case .powerMacG3Beige: "Power Macintosh G3 (Beige)"
35+
case .powerMacG4: "Power Macintosh G4 (PPC)"
36+
case .powerMacG4: "Power Macintosh G5 (PPC64)"
37+
}
38+
}
39+
40+
var architecture: QEMUArchitecture {
41+
switch self {
42+
case .quadra800: return .m68k
43+
//case .powerMacG3Beige: return .ppc
44+
case .powerMacG4: return .ppc
45+
case .powerMacG5: return .ppc64
46+
}
47+
}
48+
49+
var target: any QEMUTarget {
50+
switch self {
51+
case .quadra800: return QEMUTarget_m68k.q800
52+
//case .powerMacG3Beige: return QEMUTarget_ppc.g3beige
53+
case .powerMacG4: return QEMUTarget_ppc.mac99
54+
case .powerMacG5: return QEMUTarget_ppc.mac99
55+
}
56+
}
57+
58+
var minRam: Int {
59+
switch self {
60+
case .quadra800: return 8
61+
//case .powerMacG3Beige: return 32
62+
case .powerMacG4: return 64
63+
case .powerMacG5: return 64
64+
}
65+
}
66+
67+
var maxRam: Int {
68+
switch self {
69+
case .quadra800: return 1024
70+
//case .powerMacG3Beige: return 2047
71+
case .powerMacG4: return 2048
72+
case .powerMacG5: return 2048
73+
}
74+
}
75+
76+
var defaultRam: Int {
77+
switch self {
78+
case .quadra800: return 128
79+
//case .powerMacG3Beige: return 512
80+
case .powerMacG4: return 512
81+
case .powerMacG5: return 512
82+
}
83+
}
84+
}
2385
@ObservedObject var wizardState: VMWizardState
24-
86+
@State private var classicMacSystem: ClassicMacSystem = .powerMacG4
87+
2588
var minCores: Int {
2689
#if canImport(Virtualization)
2790
VZVirtualMachineConfiguration.minimumAllowedCPUCount
@@ -56,7 +119,7 @@ struct VMWizardHardwareView: View {
56119

57120
var body: some View {
58121
VMWizardContent("Hardware") {
59-
if !wizardState.useVirtualization {
122+
if !wizardState.useVirtualization && wizardState.operatingSystem != .ClassicMacOS {
60123
Section {
61124
VMConfigConstantPicker(selection: $wizardState.systemArchitecture)
62125
.onChange(of: wizardState.systemArchitecture) { newValue in
@@ -72,39 +135,60 @@ struct VMWizardHardwareView: View {
72135
Text("System")
73136
}
74137

138+
} else if wizardState.operatingSystem == .ClassicMacOS {
139+
Section {
140+
Picker("Machine", selection: $classicMacSystem) {
141+
ForEach(ClassicMacSystem.allCases) { system in
142+
Text(system.title).tag(system)
143+
}
144+
}.pickerStyle(.inline)
145+
.onChange(of: classicMacSystem) { newValue in
146+
wizardState.systemArchitecture = newValue.architecture
147+
wizardState.systemTarget = newValue.target
148+
wizardState.systemMemoryMib = newValue.defaultRam
149+
wizardState.systemCpuCount = 1
150+
}
151+
}
75152
}
76153
Section {
77154
RAMSlider(systemMemory: $wizardState.systemMemoryMib) { _ in
78-
if wizardState.systemMemoryMib > maxMemoryMib {
79-
wizardState.systemMemoryMib = maxMemoryMib
155+
let validMax = wizardState.operatingSystem == .ClassicMacOS ? classicMacSystem.maxRam : maxMemoryMib
156+
if wizardState.systemMemoryMib > validMax {
157+
wizardState.systemMemoryMib = validMax
158+
}
159+
let validMin = wizardState.operatingSystem == .ClassicMacOS ? classicMacSystem.minRam : 0
160+
if wizardState.systemMemoryMib < validMin {
161+
wizardState.systemMemoryMib = validMin
80162
}
81163
}
82164
} header: {
83165
Text("Memory")
84166
}
85-
86-
Section {
87-
HStack {
88-
Stepper(value: $wizardState.systemCpuCount, in: minCores...maxCores) {
89-
Text("CPU Cores")
90-
}
91-
NumberTextField("", number: $wizardState.systemCpuCount, prompt: "Default", onEditingChanged: { _ in
92-
guard wizardState.systemCpuCount != 0 else {
93-
return
94-
}
95-
if wizardState.systemCpuCount < minCores {
96-
wizardState.systemCpuCount = minCores
97-
} else if wizardState.systemCpuCount > maxCores {
98-
wizardState.systemCpuCount = maxCores
167+
168+
if wizardState.operatingSystem != .ClassicMacOS {
169+
Section {
170+
HStack {
171+
Stepper(value: $wizardState.systemCpuCount, in: minCores...maxCores) {
172+
Text("CPU Cores")
99173
}
100-
})
174+
NumberTextField("", number: $wizardState.systemCpuCount, prompt: "Default", onEditingChanged: { _ in
175+
guard wizardState.systemCpuCount != 0 else {
176+
return
177+
}
178+
if wizardState.systemCpuCount < minCores {
179+
wizardState.systemCpuCount = minCores
180+
} else if wizardState.systemCpuCount > maxCores {
181+
wizardState.systemCpuCount = maxCores
182+
}
183+
})
101184
.frame(width: 80)
102185
.multilineTextAlignment(.trailing)
186+
}
187+
} header: {
188+
Text("CPU")
103189
}
104-
} header: {
105-
Text("CPU")
106190
}
107-
191+
108192

109193

110194
if !wizardState.useAppleVirtualization && wizardState.operatingSystem == .Linux {
@@ -135,6 +219,12 @@ struct VMWizardHardwareView: View {
135219
if wizardState.legacyHardware && wizardState.systemArchitecture == .x86_64 {
136220
wizardState.systemTarget = QEMUTarget_x86_64.pc
137221
}
222+
if wizardState.operatingSystem == .ClassicMacOS {
223+
wizardState.systemArchitecture = classicMacSystem.architecture
224+
wizardState.systemTarget = classicMacSystem.target
225+
wizardState.systemMemoryMib = classicMacSystem.defaultRam
226+
wizardState.systemCpuCount = 1
227+
}
138228
}
139229
}
140230

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
//
2+
// Copyright © 2025 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+
17+
import SwiftUI
18+
19+
struct VMWizardOSClassicMacView: View {
20+
private enum PpcVia: CaseIterable, Identifiable {
21+
case pmu
22+
case pmuAdb
23+
case cuda
24+
25+
var id: Self { self }
26+
27+
var title: LocalizedStringKey {
28+
switch self {
29+
case .pmu: return "PMU"
30+
case .pmuAdb: return "PMU-ADB"
31+
case .cuda: return "CUDA"
32+
}
33+
}
34+
}
35+
36+
@ObservedObject var wizardState: VMWizardState
37+
@State private var isFileImporterPresented: Bool = false
38+
@State private var ppcVia: PpcVia = .pmu
39+
40+
var body: some View {
41+
VMWizardContent("Classic Mac OS") {
42+
DetailedSection("Boot ISO Image") {
43+
FileBrowseField(url: $wizardState.bootImageURL, isFileImporterPresented: $isFileImporterPresented, hasClearButton: false)
44+
}
45+
46+
if wizardState.systemTarget.rawValue == QEMUTarget_m68k.q800.rawValue {
47+
DetailedSection("Quadra 800 ROM") {
48+
FileBrowseField(url: $wizardState.quadra800Rom, isFileImporterPresented: $isFileImporterPresented, hasClearButton: false)
49+
}
50+
}
51+
52+
if wizardState.systemArchitecture == .ppc || wizardState.systemArchitecture == .ppc64 {
53+
DetailedSection("Advanced Options") {
54+
Picker("PMU", selection: $ppcVia) {
55+
ForEach(PpcVia.allCases) { item in
56+
Text(item.title).tag(item)
57+
}
58+
}.pickerStyle(.inline)
59+
.help("Different versions of Mac OS require different VIA option.")
60+
}
61+
}
62+
63+
if wizardState.isBusy {
64+
Spinner(size: .large)
65+
}
66+
}
67+
}
68+
}
69+
70+
#Preview {
71+
VMWizardOSClassicMacView(wizardState: VMWizardState())
72+
}

Platform/Shared/VMWizardOSView.swift

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,17 @@ struct VMWizardOSView: View {
3434
}
3535
}
3636
#endif
37+
if !wizardState.useVirtualization {
38+
Button {
39+
wizardState.operatingSystem = .ClassicMacOS
40+
wizardState.useAppleVirtualization = false
41+
wizardState.isGuestToolsInstallRequested = false
42+
wizardState.legacyHardware = false
43+
wizardState.next()
44+
} label: {
45+
OperatingSystem(imageName: "macos", name: "Classic Mac OS")
46+
}
47+
}
3748
Button {
3849
wizardState.operatingSystem = .Windows
3950
wizardState.useAppleVirtualization = false

Platform/Shared/VMWizardState.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ enum VMWizardPage: Int, Identifiable {
3030
case macOSBoot
3131
case linuxBoot
3232
case windowsBoot
33+
case classicMacOSBoot
3334
case otherBoot
3435
case hardware
3536
case drives
@@ -46,6 +47,7 @@ enum VMWizardOS: String, Identifiable {
4647
case macOS
4748
case Linux
4849
case Windows
50+
case ClassicMacOS
4951
}
5052

5153
enum VMBootDevice: Int, Identifiable {
@@ -131,6 +133,7 @@ struct AlertMessage: Identifiable {
131133
@Published var linuxBootArguments: String = ""
132134
@Published var linuxHasRosetta: Bool = false
133135
@Published var isWindows10OrHigher: Bool = true
136+
@Published var quadra800Rom: URL?
134137
@Published var systemArchitecture: QEMUArchitecture = .x86_64
135138
@Published var systemTarget: any QEMUTarget = QEMUTarget_x86_64.default
136139
#if os(macOS)
@@ -217,6 +220,8 @@ struct AlertMessage: Identifiable {
217220
nextPage = .linuxBoot
218221
case .Windows:
219222
nextPage = .windowsBoot
223+
case .ClassicMacOS:
224+
nextPage = .hardware
220225
}
221226
case .otherBoot:
222227
guard bootDevice == .none || bootImageURL != nil else {
@@ -271,6 +276,11 @@ struct AlertMessage: Identifiable {
271276
}
272277
}
273278
}
279+
if operatingSystem == .ClassicMacOS {
280+
nextPage = .classicMacOSBoot
281+
}
282+
case .classicMacOSBoot:
283+
nextPage = .drives
274284
case .drives:
275285
guard storageSizeGib > 0 else {
276286
alertMessage = AlertMessage(NSLocalizedString("Invalid drive size specified.", comment: "VMWizardState"))
@@ -348,6 +358,8 @@ struct AlertMessage: Identifiable {
348358
#endif
349359
case .Windows:
350360
config.information.iconURL = UTMConfigurationInfo.builtinIcon(named: "windows")
361+
case .ClassicMacOS:
362+
config.information.iconURL = UTMConfigurationInfo.builtinIcon(named: "macos")
351363
}
352364
if !isSkipDiskCreate {
353365
var newDisk = UTMAppleConfigurationDrive(newSize: storageSizeGib * bytesInGib / bytesInMib)
@@ -513,6 +525,8 @@ struct AlertMessage: Identifiable {
513525
case .Windows:
514526
config.information.iconURL = UTMConfigurationInfo.builtinIcon(named: "windows")
515527
config.qemu.hasRTCLocalTime = true
528+
case .ClassicMacOS:
529+
break
516530
}
517531
if bootDevice != .drive {
518532
var diskImage = UTMQemuConfigurationDrive()

Platform/iOS/VMWizardView.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ fileprivate struct WizardWrapper: View {
103103
NavigationLink(destination: WizardWrapper(page: .operatingSystem, wizardState: wizardState, onDismiss: onDismiss), tag: .operatingSystem, selection: $nextPage) {}
104104
NavigationLink(destination: WizardWrapper(page: .linuxBoot, wizardState: wizardState, onDismiss: onDismiss), tag: .linuxBoot, selection: $nextPage) {}
105105
NavigationLink(destination: WizardWrapper(page: .windowsBoot, wizardState: wizardState, onDismiss: onDismiss), tag: .windowsBoot, selection: $nextPage) {}
106+
NavigationLink(destination: WizardWrapper(page: .classicMacOSBoot, wizardState: wizardState, onDismiss: onDismiss), tag: .classicMacOSBoot, selection: $nextPage) {}
106107
NavigationLink(destination: WizardWrapper(page: .otherBoot, wizardState: wizardState, onDismiss: onDismiss), tag: .otherBoot, selection: $nextPage) {}
107108
NavigationLink(destination: WizardWrapper(page: .hardware, wizardState: wizardState, onDismiss: onDismiss), tag: .hardware, selection: $nextPage) {}
108109
NavigationLink(destination: WizardWrapper(page: .drives, wizardState: wizardState, onDismiss: onDismiss), tag: .drives, selection: $nextPage) {}
@@ -181,6 +182,8 @@ fileprivate struct WizardViewWrapper: View {
181182
VMWizardOSWindowsView(wizardState: wizardState)
182183
case .otherBoot:
183184
VMWizardOSOtherView(wizardState: wizardState)
185+
case .classicMacOSBoot:
186+
VMWizardOSClassicMacView(wizardState: wizardState)
184187
case .hardware:
185188
VMWizardHardwareView(wizardState: wizardState)
186189
case .drives:

Platform/macOS/VMWizardView.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,9 @@ struct VMWizardView: View {
6060
case .windowsBoot:
6161
VMWizardOSWindowsView(wizardState: wizardState)
6262
.transition(wizardState.slide)
63+
case .classicMacOSBoot:
64+
VMWizardOSClassicMacView(wizardState: wizardState)
65+
.transition(wizardState.slide)
6366
case .hardware:
6467
VMWizardHardwareView(wizardState: wizardState)
6568
.transition(wizardState.slide)

0 commit comments

Comments
 (0)