1use std::{
22 cmp::Ordering,
23 collections::HashMap,
24 ffi::{OsStr, OsString},
25 fs,
26 path::{Path, PathBuf},
27 process::{Command, ExitStatus},
28 str::FromStr,
29};
30
31use anyhow::{Context, Result, bail};
32use object::Architecture;
33use serde::{Deserialize, Deserializer, de::Error as _};
34use tracing::{debug, error, trace};
35
36use crate::{Binary, OSVersion, Platform, util};
37
38pub fn get_temp_dir(udid: &str) -> Result<PathBuf> {
42 let mut cmd = Command::new("xcrun");
43 cmd.arg("simctl");
44 cmd.arg("getenv");
45 cmd.arg(udid);
46 cmd.arg("TMPDIR");
47 debug!("{cmd:?}");
48 let stdout = util::command_stdout(cmd)?;
49
50 let stdout = stdout.strip_suffix(b"\n").unwrap_or(&stdout);
51 #[cfg(unix)]
52 let path = <std::ffi::OsStr as std::os::unix::ffi::OsStrExt>::from_bytes(stdout);
53 #[cfg(not(unix))]
54 let path = std::ffi::OsStr::new(std::str::from_utf8(stdout).unwrap());
55 Ok(PathBuf::from(path))
56}
57
58pub fn get_device(binary: &Binary) -> Result<(Runtime, Device)> {
63 let mut cmd = Command::new("xcrun");
69 cmd.arg("simctl");
70 cmd.arg("list");
71 cmd.arg("--json");
72 debug!("{cmd:?}");
73 let stdout = util::command_stdout(cmd)?;
74
75 let info: SimulatorInfo = serde_json::from_slice(&stdout).context("failed parsing JSON")?;
76 let mut runtimes = info.runtimes;
77 if runtimes.is_empty() {
78 bail!("no simulator runtimes found? Try running `xcrun simctl list runtimes`");
79 }
80
81 runtimes
83 .retain(|runtime| runtime.availability == Availability::Available && !runtime.is_internal);
84 if runtimes.is_empty() {
85 bail!(
86 "only unavailable simulator runtimes found? Try running `xcrun simctl list runtimes available`"
87 );
88 }
89
90 let expected_platform_name = match binary.platform() {
92 Platform::IOSSIMULATOR => "iOS",
93 Platform::TVOSSIMULATOR => "tvOS",
94 Platform::WATCHOSSIMULATOR => "watchOS",
95 Platform::VISIONOSSIMULATOR => "xrOS",
96 _ => unreachable!("unknown simulator platform"),
97 };
98 runtimes.retain(|runtime| runtime.is_platform(expected_platform_name));
99 if runtimes.is_empty() {
100 bail!(
101 "no simulator runtimes for `{expected_platform_name}` found? Try running `xcrun simctl list runtimes available {expected_platform_name}`"
102 );
103 }
104
105 let expected_arch = String::from(match binary.arch {
107 Architecture::Aarch64 => "arm64",
108 Architecture::X86_64 => "x86_64",
109 Architecture::I386 => "i386", arch => {
111 error!(?arch, "unknown simulator architecture");
112 ""
113 }
114 });
115 runtimes.retain(|runtime| {
116 runtime
117 .supported_architectures
118 .as_deref()
119 .map(|archs| archs.contains(&expected_arch))
120 .unwrap_or(true)
121 });
122 if runtimes.is_empty() {
123 bail!(
124 "no simulator runtimes for the architecture {expected_arch} found? Ensure that you're running Cargo with the `--target` flag corresponding to your host architecture"
125 );
126 }
127
128 let minos = binary.minos();
130 runtimes.retain(|runtime| match OSVersion::from_str(&runtime.version) {
131 Ok(runtime_version) => minos <= runtime_version,
132 Err(err) => {
133 error!(?runtime.version, "failed parsing: {err}");
134 true
135 }
136 });
137 if runtimes.is_empty() {
138 bail!(
139 "the binary was compiled for {expected_platform_name} {minos}, but no simulator runtimes support that high OS version. Check `xcrun simctl list runtimes available`",
140 );
141 }
142
143 trace!(?runtimes, "found runtimes");
144
145 let mut devices = runtimes
147 .iter()
148 .flat_map(|runtime| {
149 info.devices
150 .get(&runtime.identifier)
151 .or_else(|| info.devices.get(&runtime.name))
152 .map(|devices| &**devices)
153 .unwrap_or_else(|| {
154 error!("could not find devices for runtime {}", runtime.identifier);
155 &[]
156 })
157 .iter()
158 .map(move |device| (runtime, device))
159 })
160 .collect::<Vec<_>>();
161 if devices.is_empty() {
162 bail!(
163 "no simulator devices found? Run `xcrun simctl list devices {expected_platform_name}` to debug, and consider running `xcrun simctl create` to create a device"
164 );
165 }
166
167 devices.retain(|(_runtime, device)| device.availability == Availability::Available);
169 if devices.is_empty() {
170 bail!(
171 "only unavailable simulator devices found? Run `xcrun simctl list devices available {expected_platform_name}` to debug, and consider running `xcrun simctl create` to create a device"
172 );
173 }
174
175 devices.retain(|(_runtime, device)| device.state == DeviceState::Booted);
177 if devices.is_empty() {
178 bail!(
179 "no booted simulator devices found? Run `xcrun simctl list devices booted {expected_platform_name}` to debug, and consider running `xcrun simctl boot` to boot the device"
180 );
181 }
182
183 devices.sort_by(|(runtime_a, device_a), (runtime_b, device_b)| {
185 match (&device_a.last_booted_at, &device_b.last_booted_at) {
187 (Some(a), Some(b)) => a.cmp(b),
189 (Some(_), None) => Ordering::Greater,
190 (None, Some(_)) => Ordering::Less,
191 (None, None) => Ordering::Equal,
192 }
193 .then_with(|| {
194 match (
196 OSVersion::from_str(&runtime_a.version),
197 OSVersion::from_str(&runtime_b.version),
198 ) {
199 (Ok(a), Ok(b)) => a.cmp(&b),
200 _ => Ordering::Equal,
201 }
202 })
203 .then_with(|| {
204 device_a.name.cmp(&device_b.name)
206 })
207 });
208
209 trace!(?devices, "found devices");
210
211 let (runtime, device) = devices.first().expect("checked before");
213 Ok(((*runtime).clone(), (*device).clone()))
214}
215
216pub fn spawn<A: AsRef<OsStr>>(
217 udid: &str,
218 bundle_path: &Path,
219 exe_path: &Path,
220 args: impl Iterator<Item = A>,
221) -> Result<ExitStatus> {
222 let temp_dir = get_temp_dir(udid)?;
223
224 debug_assert_eq!(bundle_path, exe_path);
234
235 let mut cmd = Command::new("xcrun");
237 cmd.arg("simctl");
238 cmd.arg("spawn");
239 cmd.arg(udid);
240 cmd.arg(exe_path);
241 cmd.args(args);
242 cmd.envs(forwarded_env_vars());
243 cmd.env("SIMCTL_CHILD_CARGO_TARGET_TMPDIR", temp_dir);
245
246 debug!("{cmd:?}");
247 let status = cmd
248 .status()
249 .with_context(|| format!("failed spawning executable {exe_path:?}"))?;
250
251 Ok(status)
252}
253
254pub fn install_and_launch<A: AsRef<OsStr>>(
255 udid: &str,
256 bundle_path: &Path,
257 bundle_identifier: &str,
258 args: impl Iterator<Item = A>,
259) -> Result<ExitStatus> {
260 let temp_dir = get_temp_dir(udid)?;
261
262 let lock_file = fs::File::create(temp_dir.join("cargo-apple-runner.lock"))
270 .context("failed creating lock file in simulator")?;
271 lock_file.lock()?;
272
273 let mut cmd = Command::new("xcrun");
277 cmd.arg("simctl");
278 cmd.arg("install");
279 cmd.arg(udid);
280 cmd.arg(bundle_path);
281 debug!("{cmd:?}");
282 let _ = util::command_stdout(cmd)?;
283
284 let mut cmd = Command::new("xcrun");
286 cmd.arg("simctl");
287 cmd.arg("launch");
288 cmd.arg("--console");
289 cmd.arg(udid);
292 cmd.arg(bundle_identifier);
293 cmd.args(args);
294 cmd.envs(forwarded_env_vars());
295 cmd.env("SIMCTL_CHILD_CARGO_TARGET_TMPDIR", temp_dir);
297
298 debug!("{cmd:?}");
299 let status = cmd
300 .status()
301 .with_context(|| format!("failed launching application {bundle_identifier:?}"))?;
302
303 lock_file.unlock()?;
307 Ok(status)
308}
309
310fn forwarded_env_vars() -> impl IntoIterator<Item = (OsString, OsString)> {
324 std::env::vars_os()
325 .filter(|(key, _)| {
326 let Some(key) = key.to_str() else {
327 return false;
328 };
329
330 key.starts_with("CARGO_PKG_")
331 || matches!(
332 key,
333 "CARGO_CRATE_NAME" | "CARGO_BIN_NAME" | "CARGO_PRIMARY_PACKAGE"
334 )
335 })
336 .map(|(key, value)| {
337 let mut new_key = OsString::from("SIMCTL_CHILD_");
338 new_key.push(key);
339 (new_key, value)
340 })
341}
342
343#[derive(Deserialize)]
344struct SimulatorInfo {
345 #[serde(default)]
346 runtimes: Vec<Runtime>,
347 #[serde(default)]
349 devices: HashMap<String, Vec<Device>>,
350}
351
352#[derive(Deserialize, Clone, Debug)]
353pub struct Runtime {
354 #[allow(dead_code)]
355 buildversion: String,
356 name: String,
357 identifier: String,
358 version: String,
359 #[serde(deserialize_with = "deserialize_availability", flatten)]
360 availability: Availability,
361
362 #[serde(rename = "isInternal")]
364 #[serde(default)] is_internal: bool,
366 platform: Option<String>,
367 #[serde(rename = "supportedArchitectures")]
368 supported_architectures: Option<Vec<String>>,
369}
370
371impl Runtime {
372 fn is_platform(&self, platform_name: &str) -> bool {
373 if let Some(p) = &self.platform {
374 p == platform_name
375 } else {
376 self.identifier.contains(&platform_name)
377 }
378 }
379}
380
381#[derive(Deserialize, PartialEq, Eq, Hash, Clone, Debug)]
382pub struct Device {
383 name: String,
384 pub udid: String,
386 state: DeviceState,
387 #[serde(deserialize_with = "deserialize_availability", flatten)]
388 availability: Availability,
389
390 #[serde(rename = "lastBootedAt")]
392 last_booted_at: Option<String>,
393}
394
395#[derive(Deserialize, PartialEq, Eq, Hash, Clone, Debug)]
398enum DeviceState {
399 Creating,
400 Booting,
401 Booted,
402 #[serde(rename = "Shutting Down")]
403 ShuttingDown,
404 Shutdown,
405 #[serde(other)]
406 Unknown,
407}
408
409#[derive(PartialEq, Eq, Hash, Clone, Debug)]
410enum Availability {
411 Available,
412 Unavailable(String),
413}
414
415fn deserialize_availability<'de, D: Deserializer<'de>>(d: D) -> Result<Availability, D::Error> {
423 #[derive(Deserialize)]
424 struct Raw {
425 availability: Option<String>,
426 #[serde(rename = "isAvailable")]
427 is_available: Option<bool>,
428 #[serde(rename = "availabilityError")]
429 availability_error: Option<String>,
430 }
431
432 let raw = Raw::deserialize(d)?;
433
434 match (raw.availability, raw.is_available, raw.availability_error) {
435 (Some(s), _, _) if s == "(available)" => Ok(Availability::Available),
436 (Some(message), _, _) => Ok(Availability::Unavailable(message)),
437 (_, Some(true), _) => Ok(Availability::Available),
438 (_, Some(false), Some(message)) => Ok(Availability::Unavailable(message)),
439 (_, Some(false), None) => Ok(Availability::Unavailable(String::new())),
440 _ => Err(D::Error::custom("missing availability field")),
441 }
442}
443
444#[cfg(test)]
445mod tests;