// This is a script to deploy and execute a binary on an iOS simulator. // The primary use of this is to be able to run unit tests on the simulator and // retrieve the results. // // To do this through Cargo instead, use Dinghy // (https://github.com/snipsco/dinghy): cargo dinghy install, then cargo dinghy // test. // // Source: this script is part of libc // // https://github.com/rust-lang/libc/blob/master/ci/ios/deploy_and_run_on_ios_simulator.rs // // and should be sync'ed with it when ci breaks (or periodically). use std::env; use std::fs::{self, File}; use std::io::Write; use std::path::Path; use std::process; use std::process::Command; use std::time::{Duration, Instant}; macro_rules! t { ($e:expr) => (match $e { Ok(e) => e, Err(e) => panic!("{} failed with: {}", stringify!($e), e), }) } // Step one: Wrap as an app fn package_as_simulator_app(crate_name: &str, test_binary_path: &Path) { println!("== Packaging simulator app"); drop(fs::remove_dir_all("ios_simulator_app")); t!(fs::create_dir("ios_simulator_app")); t!(fs::copy(test_binary_path, Path::new("ios_simulator_app").join(crate_name))); let mut f = t!(File::create("ios_simulator_app/Info.plist")); t!(f.write_all(format!(r#" CFBundleExecutable {} CFBundleIdentifier com.rust.unittests "#, crate_name).as_bytes())); } #[derive(Debug, Clone)] struct SimulatorStatus { simulator_exists: bool, simulator_booted: bool, found_rust_sim: bool, stdout: String, error: String, } impl SimulatorStatus { fn eq(&self, other: &Self) -> bool { self.simulator_booted == other.simulator_booted && self.simulator_exists==other.simulator_exists && self.found_rust_sim == other.found_rust_sim } } fn check_simulator() -> SimulatorStatus { let output = t!( Command::new("xcrun") .arg("simctl") .arg("list") .output() ); let mut simulator_exists = false; let mut simulator_booted = false; let mut found_rust_sim = false; let mut error = String::new(); let stdout = if output.status.success() { String::from_utf8_lossy(&output.stdout) .to_string() } else { error.extend( format!( "Command Exec failed: {:?}", output, ).chars() ); String::new() }; for line in stdout.lines() { if line.contains("rust_ios") { if found_rust_sim { error.push_str("Duplicate rust_ios simulators found. Please double-check xcrun simctl list.\n"); break; } simulator_exists = true; simulator_booted = line.contains("(Booted)"); found_rust_sim = true; } } SimulatorStatus { simulator_exists, simulator_booted, found_rust_sim, stdout, error, } } fn wait_for_simulator( maybe_timeout: Option ) -> Result { const MAX: usize = 1024; let started = Instant::now(); let mut status = check_simulator(); if status.simulator_booted { return Ok(status); } for i in 1..=MAX { std::thread::sleep(Duration::from_secs(1)); if status.simulator_booted { return Ok(status); } let cur = check_simulator(); if cur.eq(&status) { eprintln!("iter({i}) = no change"); } else { eprintln!("iter({i}) = {status:?}"); } status = cur; if let Some(timeout) = maybe_timeout { if started.elapsed() > timeout && i > 5 { return Err( format!("timed out. {timeout:?}") ); } } } Err(format!("max iteration {MAX} reached")) } // Step two: Start the iOS simulator fn start_simulator() { println!("== Looking for iOS simulator"); let status = check_simulator(); if status.simulator_exists == false { println!("== Creating iOS simulator"); Command::new("xcrun") .arg("simctl") .arg("create") .arg("rust_ios") .arg("com.apple.CoreSimulator.SimDeviceType.iPhone-SE") .arg("com.apple.CoreSimulator.SimRuntime.iOS-10-2") .check_status(); } else if status.simulator_booted == true { println!("Shutting down already-booted simulator"); Command::new("xcrun") .arg("simctl") .arg("shutdown") .arg("rust_ios") .check_status(); } println!("Starting iOS simulator"); // We can't uninstall the app (if present) as that will hang if the // simulator isn't completely booted; just erase the simulator instead. Command::new("xcrun").arg("simctl").arg("erase").arg("rust_ios").check_status(); Command::new("xcrun").arg("simctl").arg("boot").arg("rust_ios").check_status(); wait_for_simulator( Some(Duration::from_secs(10)) ).unwrap(); } // Step three: Install the app fn install_app_to_simulator() { println!("Installing app to simulator"); Command::new("xcrun") .arg("simctl") .arg("install") .arg("booted") .arg("ios_simulator_app/") .check_status(); } // Step four: Run the app fn run_app_on_simulator() { println!("== Running app"); let output = t!(Command::new("xcrun") .arg("simctl") .arg("launch") .arg("--console") .arg("booted") .arg("com.rust.unittests") .output() ); println!("status: {}", output.status); println!("stdout --\n{}\n", String::from_utf8_lossy(&output.stdout)); println!("stderr --\n{}\n", String::from_utf8_lossy(&output.stderr)); let stdout = String::from_utf8_lossy(&output.stdout); let passed: bool = stdout.lines() .any(|l| { ( l.contains("PASSED") && l.contains("tests") ) || l.contains("test result: ok") }); println!("Shutting down simulator"); Command::new("xcrun") .arg("simctl") .arg("shutdown") .arg("rust_ios") .check_status(); if !passed { panic!("tests didn't pass"); } } trait CheckStatus { fn check_status(&mut self); } impl CheckStatus for Command { fn check_status(&mut self) { println!("\trunning: {:?}", self); assert!(t!(self.status()).success()); } } fn main() { let args: Vec = env::args().collect(); if args.len() != 2 { println!("Usage: {} ", args[0]); process::exit(-1); } let test_binary_path = Path::new(&args[1]); let crate_name = test_binary_path.file_name().unwrap(); package_as_simulator_app(crate_name.to_str().unwrap(), test_binary_path); start_simulator(); install_app_to_simulator(); run_app_on_simulator(); }