use std::collections::{BTreeSet, HashMap};
use serde::Deserialize;
use thiserror::Error;
use crate::{
DuctExpressionExt,
env::{Env, ExplicitEnv as _},
util::cli::{Report, Reportable},
};
use super::{Device, Platform};
#[derive(Debug, Error)]
pub enum DeviceListError {
#[error("Failed to request device list from `{command}`: {error}")]
DetectionFailed {
command: String,
error: std::io::Error,
},
#[error("`simctl list` returned an invalid JSON: {0}")]
InvalidDeviceList(#[from] serde_json::Error),
}
#[derive(Deserialize)]
pub struct OutputDevice {
name: String,
udid: String,
}
#[derive(Deserialize)]
struct DeviceListOutput {
devices: HashMap<String, Vec<OutputDevice>>,
}
impl Reportable for DeviceListError {
fn report(&self) -> Report {
Report::error("Failed to detect connected iOS simulators", self)
}
}
fn parse_device_list(output: &std::process::Output) -> Result<BTreeSet<Device>, DeviceListError> {
let stdout = String::from_utf8_lossy(&output.stdout);
let devices = serde_json::from_str::<DeviceListOutput>(&stdout)?
.devices
.into_iter()
.filter_map(|(k, devices)| {
k.split_once("iOS-")
.map(|(_, version)| (Platform::Ios, version.replace('-', ".")))
.or_else(|| {
k.split_once("xrOS-")
.map(|(_, version)| (Platform::Xros, version.replace('-', ".")))
})
.map(|(platform, version)| {
devices
.into_iter()
.map(|device| Device {
name: device.name,
udid: device.udid,
platform,
os_version: version.clone(),
})
.collect::<Vec<_>>()
})
})
.flatten()
.collect();
Ok(devices)
}
pub fn device_list(env: &Env) -> Result<BTreeSet<Device>, DeviceListError> {
let cmd = duct::cmd(
"xcrun",
["simctl", "list", "--json", "devices", "available"],
)
.vars(env.explicit_env())
.stdout_capture()
.stderr_capture();
match cmd.run() {
Ok(output) => {
if output.stdout.is_empty() && output.stderr.is_empty() {
log::info!(
"device detection returned a non-zero exit code, but stdout and stderr are both empty; interpreting as a successful run with no devices connected"
);
Ok(Default::default())
} else {
parse_device_list(&output)
}
}
Err(err) => Err(DeviceListError::DetectionFailed {
command: format!("{cmd:?}"),
error: err,
}),
}
}