use serde::{Deserialize, Serialize};
use crate::{AppleDiscovery, AppleDiscoveryOptions, IOS_SIMULATOR_TARGET, discover_apple};
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum DoctorStatus {
Passed,
Warning,
Failed,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct AppleDoctorCheck {
pub id: String,
pub name: String,
pub status: DoctorStatus,
pub detail: String,
pub fix: Option<String>,
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct AppleDoctorOptions {
pub discovery: AppleDiscoveryOptions,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct AppleDoctorReport {
pub schema_version: u32,
pub ready_for_simulator_build: bool,
pub ready_for_simulator_run: bool,
pub checks: Vec<AppleDoctorCheck>,
pub discovery: Option<AppleDiscovery>,
}
#[allow(clippy::too_many_lines)]
pub fn doctor_apple(options: &AppleDoctorOptions) -> AppleDoctorReport {
let discovery = match discover_apple(&options.discovery) {
Ok(discovery) => discovery,
Err(error) => {
return AppleDoctorReport {
schema_version: 1,
ready_for_simulator_build: false,
ready_for_simulator_run: false,
checks: vec![fail(
"discovery",
"Apple toolchain discovery",
error.to_string(),
"Use an existing project directory and rerun `cargo ferry doctor`.",
)],
discovery: None,
};
}
};
let mut checks = Vec::new();
if discovery.host_os == "macos" {
checks.push(pass(
"host",
"Build host",
format!("macOS ({})", discovery.host_arch),
));
} else {
checks.push(fail(
"host",
"Build host",
discovery.host_os.clone(),
"Build iOS applications on macOS with full Xcode installed.",
));
}
match (&discovery.developer_dir, &discovery.xcode_version) {
(Some(path), Some(version))
if path
.join("Platforms/iPhoneSimulator.platform/Developer/SDKs")
.is_dir() =>
{
checks.push(pass(
"xcode",
"Full Xcode",
format!("{} ({path})", version.replace('\n', ", ")),
));
}
(path, version) => checks.push(fail(
"xcode",
"Full Xcode",
format!("developer directory: {path:?}; version: {version:?}"),
"Install Xcode from Apple, select it with xcode-select, then rerun `cargo ferry doctor`.",
)),
}
executable_check(
&mut checks,
"xcodebuild",
"xcodebuild",
discovery.host_tools.xcodebuild.as_ref(),
"Install full Xcode and select its developer directory.",
);
executable_check(
&mut checks,
"xcrun",
"xcrun",
discovery.host_tools.xcrun.as_ref(),
"Install full Xcode and select its developer directory.",
);
executable_check(
&mut checks,
"plutil",
"plutil",
discovery.host_tools.plutil.as_ref(),
"Restore the macOS command-line tools or reinstall Xcode.",
);
executable_check(
&mut checks,
"cargo",
"Cargo",
discovery.host_tools.cargo.as_ref(),
"Install Rust with rustup.",
);
executable_check(
&mut checks,
"rustup",
"rustup",
discovery.host_tools.rustup.as_ref(),
"Install rustup so iOS Rust targets can be managed.",
);
if let Some(sdk) = &discovery.simulator_sdk {
checks.push(pass(
"iphonesimulator-sdk",
"iPhone Simulator SDK",
format!("{} ({})", sdk.version, sdk.path),
));
} else {
checks.push(fail(
"iphonesimulator-sdk",
"iPhone Simulator SDK",
"xcrun did not return an installed SDK".to_owned(),
"Open Xcode Settings > Platforms and install an iOS platform SDK.",
));
}
if discovery
.installed_rust_targets
.iter()
.any(|target| target == IOS_SIMULATOR_TARGET)
{
checks.push(pass(
"rust-target-ios-simulator",
format!("Rust target {IOS_SIMULATOR_TARGET}"),
"installed".to_owned(),
));
} else {
checks.push(fail(
"rust-target-ios-simulator",
format!("Rust target {IOS_SIMULATOR_TARGET}"),
"not installed".to_owned(),
format!("Run `rustup target add {IOS_SIMULATOR_TARGET}`."),
));
}
let available_runtimes = discovery
.simulator_runtimes
.iter()
.filter(|runtime| runtime.available)
.collect::<Vec<_>>();
let ready_for_simulator_run = !available_runtimes.is_empty();
if ready_for_simulator_run {
checks.push(pass(
"simulator-runtime",
"Simulator runtime",
available_runtimes
.iter()
.map(|runtime| format!("{} {}", runtime.name, runtime.version))
.collect::<Vec<_>>()
.join(", "),
));
} else {
checks.push(warn(
"simulator-runtime",
"Simulator runtime",
"no available runtime; build-only remains supported".to_owned(),
Some(
"Install a runtime in Xcode Settings > Platforms only for install/run validation."
.to_owned(),
),
));
}
if discovery.host_arch != "aarch64" {
checks.push(warn(
"host-architecture",
"Simulator host architecture",
format!(
"{} host; cargo-ferry currently emits an arm64 simulator executable",
discovery.host_arch
),
Some(
"Build-only can cross-compile, but run validation requires an Apple Silicon Mac."
.to_owned(),
),
));
}
let ready_for_simulator_build = checks
.iter()
.all(|check| check.status != DoctorStatus::Failed);
AppleDoctorReport {
schema_version: 1,
ready_for_simulator_build,
ready_for_simulator_run,
checks,
discovery: Some(discovery),
}
}
fn executable_check(
checks: &mut Vec<AppleDoctorCheck>,
id: &str,
name: &str,
path: Option<&camino::Utf8PathBuf>,
fix: &str,
) {
if let Some(path) = path {
checks.push(pass(id, name, path.to_string()));
} else {
checks.push(fail(id, name, "not found".to_owned(), fix.to_owned()));
}
}
fn pass(
id: impl Into<String>,
name: impl Into<String>,
detail: impl Into<String>,
) -> AppleDoctorCheck {
AppleDoctorCheck {
id: id.into(),
name: name.into(),
status: DoctorStatus::Passed,
detail: detail.into(),
fix: None,
}
}
fn warn(
id: impl Into<String>,
name: impl Into<String>,
detail: impl Into<String>,
fix: Option<String>,
) -> AppleDoctorCheck {
AppleDoctorCheck {
id: id.into(),
name: name.into(),
status: DoctorStatus::Warning,
detail: detail.into(),
fix,
}
}
fn fail(
id: impl Into<String>,
name: impl Into<String>,
detail: impl Into<String>,
fix: impl Into<String>,
) -> AppleDoctorCheck {
AppleDoctorCheck {
id: id.into(),
name: name.into(),
status: DoctorStatus::Failed,
detail: detail.into(),
fix: Some(fix.into()),
}
}