1use serde::{Deserialize, Serialize};
2
3use crate::{AppleDiscovery, AppleDiscoveryOptions, IOS_SIMULATOR_TARGET, discover_apple};
4
5#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
7#[serde(rename_all = "kebab-case")]
8pub enum DoctorStatus {
9 Passed,
11 Warning,
13 Failed,
15}
16
17#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
19pub struct AppleDoctorCheck {
20 pub id: String,
22 pub name: String,
24 pub status: DoctorStatus,
26 pub detail: String,
28 pub fix: Option<String>,
30}
31
32#[derive(Clone, Debug, Default, Eq, PartialEq)]
34pub struct AppleDoctorOptions {
35 pub discovery: AppleDiscoveryOptions,
37}
38
39#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
41pub struct AppleDoctorReport {
42 pub schema_version: u32,
44 pub ready_for_simulator_build: bool,
46 pub ready_for_simulator_run: bool,
48 pub checks: Vec<AppleDoctorCheck>,
50 pub discovery: Option<AppleDiscovery>,
52}
53
54#[allow(clippy::too_many_lines)]
56pub fn doctor_apple(options: &AppleDoctorOptions) -> AppleDoctorReport {
57 let discovery = match discover_apple(&options.discovery) {
58 Ok(discovery) => discovery,
59 Err(error) => {
60 return AppleDoctorReport {
61 schema_version: 1,
62 ready_for_simulator_build: false,
63 ready_for_simulator_run: false,
64 checks: vec![fail(
65 "discovery",
66 "Apple toolchain discovery",
67 error.to_string(),
68 "Use an existing project directory and rerun `cargo ferry doctor`.",
69 )],
70 discovery: None,
71 };
72 }
73 };
74
75 let mut checks = Vec::new();
76 if discovery.host_os == "macos" {
77 checks.push(pass(
78 "host",
79 "Build host",
80 format!("macOS ({})", discovery.host_arch),
81 ));
82 } else {
83 checks.push(fail(
84 "host",
85 "Build host",
86 discovery.host_os.clone(),
87 "Build iOS applications on macOS with full Xcode installed.",
88 ));
89 }
90
91 match (&discovery.developer_dir, &discovery.xcode_version) {
92 (Some(path), Some(version))
93 if path
94 .join("Platforms/iPhoneSimulator.platform/Developer/SDKs")
95 .is_dir() =>
96 {
97 checks.push(pass(
98 "xcode",
99 "Full Xcode",
100 format!("{} ({path})", version.replace('\n', ", ")),
101 ));
102 }
103 (path, version) => checks.push(fail(
104 "xcode",
105 "Full Xcode",
106 format!("developer directory: {path:?}; version: {version:?}"),
107 "Install Xcode from Apple, select it with xcode-select, then rerun `cargo ferry doctor`.",
108 )),
109 }
110
111 executable_check(
112 &mut checks,
113 "xcodebuild",
114 "xcodebuild",
115 discovery.host_tools.xcodebuild.as_ref(),
116 "Install full Xcode and select its developer directory.",
117 );
118 executable_check(
119 &mut checks,
120 "xcrun",
121 "xcrun",
122 discovery.host_tools.xcrun.as_ref(),
123 "Install full Xcode and select its developer directory.",
124 );
125 executable_check(
126 &mut checks,
127 "plutil",
128 "plutil",
129 discovery.host_tools.plutil.as_ref(),
130 "Restore the macOS command-line tools or reinstall Xcode.",
131 );
132 executable_check(
133 &mut checks,
134 "cargo",
135 "Cargo",
136 discovery.host_tools.cargo.as_ref(),
137 "Install Rust with rustup.",
138 );
139 executable_check(
140 &mut checks,
141 "rustup",
142 "rustup",
143 discovery.host_tools.rustup.as_ref(),
144 "Install rustup so iOS Rust targets can be managed.",
145 );
146
147 if let Some(sdk) = &discovery.simulator_sdk {
148 checks.push(pass(
149 "iphonesimulator-sdk",
150 "iPhone Simulator SDK",
151 format!("{} ({})", sdk.version, sdk.path),
152 ));
153 } else {
154 checks.push(fail(
155 "iphonesimulator-sdk",
156 "iPhone Simulator SDK",
157 "xcrun did not return an installed SDK".to_owned(),
158 "Open Xcode Settings > Platforms and install an iOS platform SDK.",
159 ));
160 }
161
162 if discovery
163 .installed_rust_targets
164 .iter()
165 .any(|target| target == IOS_SIMULATOR_TARGET)
166 {
167 checks.push(pass(
168 "rust-target-ios-simulator",
169 format!("Rust target {IOS_SIMULATOR_TARGET}"),
170 "installed".to_owned(),
171 ));
172 } else {
173 checks.push(fail(
174 "rust-target-ios-simulator",
175 format!("Rust target {IOS_SIMULATOR_TARGET}"),
176 "not installed".to_owned(),
177 format!("Run `rustup target add {IOS_SIMULATOR_TARGET}`."),
178 ));
179 }
180
181 let available_runtimes = discovery
182 .simulator_runtimes
183 .iter()
184 .filter(|runtime| runtime.available)
185 .collect::<Vec<_>>();
186 let ready_for_simulator_run = !available_runtimes.is_empty();
187 if ready_for_simulator_run {
188 checks.push(pass(
189 "simulator-runtime",
190 "Simulator runtime",
191 available_runtimes
192 .iter()
193 .map(|runtime| format!("{} {}", runtime.name, runtime.version))
194 .collect::<Vec<_>>()
195 .join(", "),
196 ));
197 } else {
198 checks.push(warn(
199 "simulator-runtime",
200 "Simulator runtime",
201 "no available runtime; build-only remains supported".to_owned(),
202 Some(
203 "Install a runtime in Xcode Settings > Platforms only for install/run validation."
204 .to_owned(),
205 ),
206 ));
207 }
208
209 if discovery.host_arch != "aarch64" {
210 checks.push(warn(
211 "host-architecture",
212 "Simulator host architecture",
213 format!(
214 "{} host; cargo-ferry currently emits an arm64 simulator executable",
215 discovery.host_arch
216 ),
217 Some(
218 "Build-only can cross-compile, but run validation requires an Apple Silicon Mac."
219 .to_owned(),
220 ),
221 ));
222 }
223
224 let ready_for_simulator_build = checks
225 .iter()
226 .all(|check| check.status != DoctorStatus::Failed);
227 AppleDoctorReport {
228 schema_version: 1,
229 ready_for_simulator_build,
230 ready_for_simulator_run,
231 checks,
232 discovery: Some(discovery),
233 }
234}
235
236fn executable_check(
237 checks: &mut Vec<AppleDoctorCheck>,
238 id: &str,
239 name: &str,
240 path: Option<&camino::Utf8PathBuf>,
241 fix: &str,
242) {
243 if let Some(path) = path {
244 checks.push(pass(id, name, path.to_string()));
245 } else {
246 checks.push(fail(id, name, "not found".to_owned(), fix.to_owned()));
247 }
248}
249
250fn pass(
251 id: impl Into<String>,
252 name: impl Into<String>,
253 detail: impl Into<String>,
254) -> AppleDoctorCheck {
255 AppleDoctorCheck {
256 id: id.into(),
257 name: name.into(),
258 status: DoctorStatus::Passed,
259 detail: detail.into(),
260 fix: None,
261 }
262}
263
264fn warn(
265 id: impl Into<String>,
266 name: impl Into<String>,
267 detail: impl Into<String>,
268 fix: Option<String>,
269) -> AppleDoctorCheck {
270 AppleDoctorCheck {
271 id: id.into(),
272 name: name.into(),
273 status: DoctorStatus::Warning,
274 detail: detail.into(),
275 fix,
276 }
277}
278
279fn fail(
280 id: impl Into<String>,
281 name: impl Into<String>,
282 detail: impl Into<String>,
283 fix: impl Into<String>,
284) -> AppleDoctorCheck {
285 AppleDoctorCheck {
286 id: id.into(),
287 name: name.into(),
288 status: DoctorStatus::Failed,
289 detail: detail.into(),
290 fix: Some(fix.into()),
291 }
292}