ios.rs 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275
  1. // Copyright 2019-2021 Tauri Programme within The Commons Conservancy
  2. // SPDX-License-Identifier: Apache-2.0
  3. // SPDX-License-Identifier: MIT
  4. use cargo_mobile::{
  5. apple::{
  6. config::{
  7. Config as AppleConfig, Metadata as AppleMetadata, Platform as ApplePlatform,
  8. Raw as RawAppleConfig,
  9. },
  10. device::Device,
  11. ios_deploy, simctl,
  12. target::Target,
  13. teams::find_development_teams,
  14. },
  15. config::app::App,
  16. env::Env,
  17. opts::NoiseLevel,
  18. os,
  19. util::prompt,
  20. };
  21. use clap::{Parser, Subcommand};
  22. use sublime_fuzzy::best_match;
  23. use super::{
  24. ensure_init, env, get_app,
  25. init::{command as init_command, init_dot_cargo},
  26. log_finished, read_options, CliOptions, Target as MobileTarget, MIN_DEVICE_MATCH_SCORE,
  27. };
  28. use crate::{
  29. helpers::config::{get as get_tauri_config, Config as TauriConfig},
  30. Result,
  31. };
  32. use std::{
  33. process::exit,
  34. thread::{sleep, spawn},
  35. time::Duration,
  36. };
  37. mod build;
  38. mod dev;
  39. mod open;
  40. pub(crate) mod project;
  41. mod xcode_script;
  42. pub const APPLE_DEVELOPMENT_TEAM_ENV_VAR_NAME: &str = "TAURI_APPLE_DEVELOPMENT_TEAM";
  43. #[derive(Parser)]
  44. #[clap(
  45. author,
  46. version,
  47. about = "iOS commands",
  48. subcommand_required(true),
  49. arg_required_else_help(true)
  50. )]
  51. pub struct Cli {
  52. #[clap(subcommand)]
  53. command: Commands,
  54. }
  55. #[derive(Debug, Parser)]
  56. #[clap(about = "Initializes a Tauri iOS project")]
  57. pub struct InitOptions {
  58. /// Skip prompting for values
  59. #[clap(long)]
  60. ci: bool,
  61. /// Reinstall dependencies
  62. #[clap(short, long)]
  63. reinstall_deps: bool,
  64. }
  65. #[derive(Subcommand)]
  66. enum Commands {
  67. Init(InitOptions),
  68. Open,
  69. Dev(dev::Options),
  70. Build(build::Options),
  71. #[clap(hide(true))]
  72. XcodeScript(xcode_script::Options),
  73. }
  74. pub fn command(cli: Cli, verbosity: u8) -> Result<()> {
  75. let noise_level = NoiseLevel::from_occurrences(verbosity as u64);
  76. match cli.command {
  77. Commands::Init(options) => init_command(MobileTarget::Ios, options.ci, options.reinstall_deps)?,
  78. Commands::Open => open::command()?,
  79. Commands::Dev(options) => dev::command(options, noise_level)?,
  80. Commands::Build(options) => build::command(options, noise_level)?,
  81. Commands::XcodeScript(options) => xcode_script::command(options)?,
  82. }
  83. Ok(())
  84. }
  85. pub fn get_config(
  86. app: Option<App>,
  87. config: &TauriConfig,
  88. cli_options: &CliOptions,
  89. ) -> (App, AppleConfig, AppleMetadata) {
  90. let app = app.unwrap_or_else(|| get_app(config));
  91. let ios_options = cli_options.clone();
  92. let raw = RawAppleConfig {
  93. development_team: std::env::var(APPLE_DEVELOPMENT_TEAM_ENV_VAR_NAME)
  94. .ok()
  95. .or_else(|| config.tauri.bundle.ios.development_team.clone())
  96. .unwrap_or_else(|| {
  97. let teams = find_development_teams().unwrap_or_default();
  98. match teams.len() {
  99. 0 => {
  100. log::error!("No code signing certificates found. You must add one and set the certificate development team ID on the `tauri > iOS > developmentTeam` config value or the `{APPLE_DEVELOPMENT_TEAM_ENV_VAR_NAME}` environment variable. To list the available certificates, run `tauri info`.");
  101. exit(1);
  102. }
  103. 1 => teams.first().unwrap().id.clone(),
  104. _ => {
  105. log::error!("You must set the code signing certificate development team ID on the `tauri > iOS > developmentTeam` config value or the `{APPLE_DEVELOPMENT_TEAM_ENV_VAR_NAME}` environment variable. Available certificates: {}", teams.iter().map(|t| format!("{} (ID: {})", t.name, t.id)).collect::<Vec<String>>().join(", "));
  106. exit(1);
  107. }
  108. }
  109. }),
  110. ios_features: ios_options.features.clone(),
  111. bundle_version: config.package.version.clone(),
  112. bundle_version_short: config.package.version.clone(),
  113. ..Default::default()
  114. };
  115. let config = AppleConfig::from_raw(app.clone(), Some(raw)).unwrap();
  116. let metadata = AppleMetadata {
  117. supported: true,
  118. ios: ApplePlatform {
  119. cargo_args: Some(ios_options.args),
  120. features: ios_options.features,
  121. ..Default::default()
  122. },
  123. macos: Default::default(),
  124. };
  125. (app, config, metadata)
  126. }
  127. fn with_config<T>(
  128. cli_options: Option<CliOptions>,
  129. f: impl FnOnce(&App, &AppleConfig, &AppleMetadata, CliOptions) -> Result<T>,
  130. ) -> Result<T> {
  131. let (app, config, metadata, cli_options) = {
  132. let tauri_config = get_tauri_config(None)?;
  133. let tauri_config_guard = tauri_config.lock().unwrap();
  134. let tauri_config_ = tauri_config_guard.as_ref().unwrap();
  135. let cli_options = cli_options.unwrap_or_else(read_options);
  136. let (app, config, metadata) = get_config(None, tauri_config_, &cli_options);
  137. (app, config, metadata, cli_options)
  138. };
  139. f(&app, &config, &metadata, cli_options)
  140. }
  141. fn ios_deploy_device_prompt<'a>(env: &'_ Env, target: Option<&str>) -> Result<Device<'a>> {
  142. let device_list = ios_deploy::device_list(env)
  143. .map_err(|cause| anyhow::anyhow!("Failed to detect connected iOS devices: {cause}"))?;
  144. if !device_list.is_empty() {
  145. let device = if let Some(t) = target {
  146. let (device, score) = device_list
  147. .into_iter()
  148. .rev()
  149. .map(|d| {
  150. let score = best_match(t, d.name()).map_or(0, |m| m.score());
  151. (d, score)
  152. })
  153. .max_by_key(|(_, score)| *score)
  154. // we already checked the list is not empty
  155. .unwrap();
  156. if score > MIN_DEVICE_MATCH_SCORE {
  157. device
  158. } else {
  159. anyhow::bail!("Could not find an iOS device matching {t}")
  160. }
  161. } else {
  162. let index = if device_list.len() > 1 {
  163. prompt::list(
  164. concat!("Detected ", "iOS", " devices"),
  165. device_list.iter(),
  166. "device",
  167. None,
  168. "Device",
  169. )
  170. .map_err(|cause| anyhow::anyhow!("Failed to prompt for iOS device: {cause}"))?
  171. } else {
  172. 0
  173. };
  174. device_list.into_iter().nth(index).unwrap()
  175. };
  176. println!(
  177. "Detected connected device: {} with target {:?}",
  178. device,
  179. device.target().triple,
  180. );
  181. Ok(device)
  182. } else {
  183. Err(anyhow::anyhow!("No connected iOS devices detected"))
  184. }
  185. }
  186. fn simulator_prompt(env: &'_ Env, target: Option<&str>) -> Result<simctl::Device> {
  187. let simulator_list = simctl::device_list(env).map_err(|cause| {
  188. anyhow::anyhow!("Failed to detect connected iOS Simulator devices: {cause}")
  189. })?;
  190. if !simulator_list.is_empty() {
  191. let device = if let Some(t) = target {
  192. let (device, score) = simulator_list
  193. .into_iter()
  194. .rev()
  195. .map(|d| {
  196. let score = best_match(t, d.name()).map_or(0, |m| m.score());
  197. (d, score)
  198. })
  199. .max_by_key(|(_, score)| *score)
  200. // we already checked the list is not empty
  201. .unwrap();
  202. if score > MIN_DEVICE_MATCH_SCORE {
  203. device
  204. } else {
  205. anyhow::bail!("Could not find an iOS Simulator matching {t}")
  206. }
  207. } else if simulator_list.len() > 1 {
  208. let index = prompt::list(
  209. concat!("Detected ", "iOS", " simulators"),
  210. simulator_list.iter(),
  211. "simulator",
  212. None,
  213. "Simulator",
  214. )
  215. .map_err(|cause| anyhow::anyhow!("Failed to prompt for iOS Simulator device: {cause}"))?;
  216. simulator_list.into_iter().nth(index).unwrap()
  217. } else {
  218. simulator_list.into_iter().next().unwrap()
  219. };
  220. log::info!("Starting simulator {}", device.name());
  221. let handle = device.start(env)?;
  222. spawn(move || {
  223. let _ = handle.wait();
  224. });
  225. Ok(device)
  226. } else {
  227. Err(anyhow::anyhow!("No available iOS Simulator detected"))
  228. }
  229. }
  230. fn device_prompt<'a>(env: &'_ Env, target: Option<&str>) -> Result<Device<'a>> {
  231. if let Ok(device) = ios_deploy_device_prompt(env, target) {
  232. Ok(device)
  233. } else {
  234. let simulator = simulator_prompt(env, target)?;
  235. let handle = simulator.start(env)?;
  236. spawn(move || {
  237. let _ = handle.wait();
  238. });
  239. Ok(simulator.into())
  240. }
  241. }
  242. fn detect_target_ok<'a>(env: &Env) -> Option<&'a Target<'a>> {
  243. device_prompt(env, None).map(|device| device.target()).ok()
  244. }
  245. fn open_and_wait(config: &AppleConfig, env: &Env) -> ! {
  246. log::info!("Opening Xcode");
  247. if let Err(e) = os::open_file_with("Xcode", config.project_dir(), env) {
  248. log::error!("{}", e);
  249. }
  250. loop {
  251. sleep(Duration::from_secs(24 * 60 * 60));
  252. }
  253. }