mod.rs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393
  1. // Copyright 2019-2024 Tauri Programme within The Commons Conservancy
  2. // SPDX-License-Identifier: Apache-2.0
  3. // SPDX-License-Identifier: MIT
  4. use cargo_mobile2::{
  5. apple::{
  6. config::{
  7. Config as AppleConfig, Metadata as AppleMetadata, Platform as ApplePlatform,
  8. Raw as RawAppleConfig,
  9. },
  10. device::{self, Device},
  11. target::Target,
  12. teams::find_development_teams,
  13. },
  14. config::app::{App, DEFAULT_ASSET_DIR},
  15. env::Env,
  16. opts::NoiseLevel,
  17. os,
  18. util::{prompt, relativize_path},
  19. };
  20. use clap::{Parser, Subcommand};
  21. use sublime_fuzzy::best_match;
  22. use super::{
  23. ensure_init, env, get_app,
  24. init::{command as init_command, configure_cargo},
  25. log_finished, read_options, CliOptions, OptionsHandle, Target as MobileTarget,
  26. MIN_DEVICE_MATCH_SCORE,
  27. };
  28. use crate::{
  29. helpers::{app_paths::tauri_dir, config::Config as TauriConfig},
  30. Result,
  31. };
  32. use std::{
  33. env::{set_var, var_os},
  34. fs::create_dir_all,
  35. path::{Path, PathBuf},
  36. thread::sleep,
  37. time::Duration,
  38. };
  39. mod build;
  40. mod dev;
  41. pub(crate) mod project;
  42. mod xcode_script;
  43. pub const APPLE_DEVELOPMENT_TEAM_ENV_VAR_NAME: &str = "APPLE_DEVELOPMENT_TEAM";
  44. const TARGET_IOS_VERSION: &str = "13.0";
  45. #[derive(Parser)]
  46. #[clap(
  47. author,
  48. version,
  49. about = "iOS commands",
  50. subcommand_required(true),
  51. arg_required_else_help(true)
  52. )]
  53. pub struct Cli {
  54. #[clap(subcommand)]
  55. command: Commands,
  56. }
  57. #[derive(Debug, Parser)]
  58. #[clap(about = "Initialize iOS target in the project")]
  59. pub struct InitOptions {
  60. /// Skip prompting for values
  61. #[clap(long, env = "CI")]
  62. ci: bool,
  63. /// Reinstall dependencies
  64. #[clap(short, long)]
  65. reinstall_deps: bool,
  66. /// Skips installing rust toolchains via rustup
  67. #[clap(long)]
  68. skip_targets_install: bool,
  69. }
  70. #[derive(Subcommand)]
  71. enum Commands {
  72. Init(InitOptions),
  73. Dev(dev::Options),
  74. Build(build::Options),
  75. #[clap(hide(true))]
  76. XcodeScript(xcode_script::Options),
  77. }
  78. pub fn command(cli: Cli, verbosity: u8) -> Result<()> {
  79. let noise_level = NoiseLevel::from_occurrences(verbosity as u64);
  80. match cli.command {
  81. Commands::Init(options) => init_command(
  82. MobileTarget::Ios,
  83. options.ci,
  84. options.reinstall_deps,
  85. options.skip_targets_install,
  86. )?,
  87. Commands::Dev(options) => dev::command(options, noise_level)?,
  88. Commands::Build(options) => build::command(options, noise_level)?,
  89. Commands::XcodeScript(options) => xcode_script::command(options)?,
  90. }
  91. Ok(())
  92. }
  93. pub fn get_config(
  94. app: &App,
  95. tauri_config: &TauriConfig,
  96. features: Option<&Vec<String>>,
  97. cli_options: &CliOptions,
  98. ) -> (AppleConfig, AppleMetadata) {
  99. let mut ios_options = cli_options.clone();
  100. if let Some(features) = features {
  101. ios_options
  102. .features
  103. .get_or_insert(Vec::new())
  104. .extend_from_slice(features);
  105. }
  106. let raw = RawAppleConfig {
  107. development_team: std::env::var(APPLE_DEVELOPMENT_TEAM_ENV_VAR_NAME)
  108. .ok()
  109. .or_else(|| tauri_config.bundle.ios.development_team.clone())
  110. .or_else(|| {
  111. let teams = find_development_teams().unwrap_or_default();
  112. match teams.len() {
  113. 0 => {
  114. log::warn!("No code signing certificates found. You must add one and set the certificate development team ID on the `bundle > iOS > developmentTeam` config value or the `{APPLE_DEVELOPMENT_TEAM_ENV_VAR_NAME}` environment variable. To list the available certificates, run `tauri info`.");
  115. None
  116. }
  117. 1 => Some(teams.first().unwrap().id.clone()),
  118. _ => {
  119. log::warn!("You must set the code signing certificate development team ID on the `bundle > 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(", "));
  120. None
  121. }
  122. }
  123. }),
  124. ios_features: ios_options.features.clone(),
  125. bundle_version: tauri_config.version.clone(),
  126. bundle_version_short: tauri_config.version.clone(),
  127. ios_version: Some(TARGET_IOS_VERSION.into()),
  128. ..Default::default()
  129. };
  130. let config = AppleConfig::from_raw(app.clone(), Some(raw)).unwrap();
  131. let tauri_dir = tauri_dir();
  132. let mut vendor_frameworks = Vec::new();
  133. let mut frameworks = Vec::new();
  134. for framework in tauri_config
  135. .bundle
  136. .ios
  137. .frameworks
  138. .clone()
  139. .unwrap_or_default()
  140. {
  141. let framework_path = PathBuf::from(&framework);
  142. let ext = framework_path.extension().unwrap_or_default();
  143. if ext.is_empty() {
  144. frameworks.push(framework);
  145. } else if ext == "framework" {
  146. frameworks.push(
  147. framework_path
  148. .file_stem()
  149. .unwrap()
  150. .to_string_lossy()
  151. .to_string(),
  152. );
  153. } else {
  154. vendor_frameworks.push(
  155. relativize_path(tauri_dir.join(framework_path), config.project_dir())
  156. .to_string_lossy()
  157. .to_string(),
  158. );
  159. }
  160. }
  161. let metadata = AppleMetadata {
  162. supported: true,
  163. ios: ApplePlatform {
  164. cargo_args: Some(ios_options.args),
  165. features: ios_options.features,
  166. frameworks: Some(frameworks),
  167. vendor_frameworks: Some(vendor_frameworks),
  168. ..Default::default()
  169. },
  170. macos: Default::default(),
  171. };
  172. set_var("TAURI_IOS_PROJECT_PATH", config.project_dir());
  173. set_var("TAURI_IOS_APP_NAME", config.app().name());
  174. (config, metadata)
  175. }
  176. fn connected_device_prompt<'a>(env: &'_ Env, target: Option<&str>) -> Result<Device<'a>> {
  177. let device_list = device::list_devices(env)
  178. .map_err(|cause| anyhow::anyhow!("Failed to detect connected iOS devices: {cause}"))?;
  179. if !device_list.is_empty() {
  180. let device = if let Some(t) = target {
  181. let (device, score) = device_list
  182. .into_iter()
  183. .rev()
  184. .map(|d| {
  185. let score = best_match(t, d.name()).map_or(0, |m| m.score());
  186. (d, score)
  187. })
  188. .max_by_key(|(_, score)| *score)
  189. // we already checked the list is not empty
  190. .unwrap();
  191. if score > MIN_DEVICE_MATCH_SCORE {
  192. device
  193. } else {
  194. anyhow::bail!("Could not find an iOS device matching {t}")
  195. }
  196. } else {
  197. let index = if device_list.len() > 1 {
  198. prompt::list(
  199. concat!("Detected ", "iOS", " devices"),
  200. device_list.iter(),
  201. "device",
  202. None,
  203. "Device",
  204. )
  205. .map_err(|cause| anyhow::anyhow!("Failed to prompt for iOS device: {cause}"))?
  206. } else {
  207. 0
  208. };
  209. device_list.into_iter().nth(index).unwrap()
  210. };
  211. println!(
  212. "Detected connected device: {} with target {:?}",
  213. device,
  214. device.target().triple,
  215. );
  216. Ok(device)
  217. } else {
  218. Err(anyhow::anyhow!("No connected iOS devices detected"))
  219. }
  220. }
  221. fn simulator_prompt(env: &'_ Env, target: Option<&str>) -> Result<device::Simulator> {
  222. let simulator_list = device::list_simulators(env).map_err(|cause| {
  223. anyhow::anyhow!("Failed to detect connected iOS Simulator devices: {cause}")
  224. })?;
  225. if !simulator_list.is_empty() {
  226. let device = if let Some(t) = target {
  227. let (device, score) = simulator_list
  228. .into_iter()
  229. .rev()
  230. .map(|d| {
  231. let score = best_match(t, d.name()).map_or(0, |m| m.score());
  232. (d, score)
  233. })
  234. .max_by_key(|(_, score)| *score)
  235. // we already checked the list is not empty
  236. .unwrap();
  237. if score > MIN_DEVICE_MATCH_SCORE {
  238. device
  239. } else {
  240. anyhow::bail!("Could not find an iOS Simulator matching {t}")
  241. }
  242. } else if simulator_list.len() > 1 {
  243. let index = prompt::list(
  244. concat!("Detected ", "iOS", " simulators"),
  245. simulator_list.iter(),
  246. "simulator",
  247. None,
  248. "Simulator",
  249. )
  250. .map_err(|cause| anyhow::anyhow!("Failed to prompt for iOS Simulator device: {cause}"))?;
  251. simulator_list.into_iter().nth(index).unwrap()
  252. } else {
  253. simulator_list.into_iter().next().unwrap()
  254. };
  255. Ok(device)
  256. } else {
  257. Err(anyhow::anyhow!("No available iOS Simulator detected"))
  258. }
  259. }
  260. fn device_prompt<'a>(env: &'_ Env, target: Option<&str>) -> Result<Device<'a>> {
  261. if let Ok(device) = connected_device_prompt(env, target) {
  262. Ok(device)
  263. } else {
  264. let simulator = simulator_prompt(env, target)?;
  265. log::info!("Starting simulator {}", simulator.name());
  266. simulator.start_detached(env)?;
  267. Ok(simulator.into())
  268. }
  269. }
  270. fn detect_target_ok<'a>(env: &Env) -> Option<&'a Target<'a>> {
  271. device_prompt(env, None).map(|device| device.target()).ok()
  272. }
  273. fn open_and_wait(config: &AppleConfig, env: &Env) -> ! {
  274. log::info!("Opening Xcode");
  275. if let Err(e) = os::open_file_with("Xcode", config.project_dir(), env) {
  276. log::error!("{}", e);
  277. }
  278. loop {
  279. sleep(Duration::from_secs(24 * 60 * 60));
  280. }
  281. }
  282. fn inject_assets(config: &AppleConfig) -> Result<()> {
  283. let asset_dir = config.project_dir().join(DEFAULT_ASSET_DIR);
  284. create_dir_all(asset_dir)?;
  285. Ok(())
  286. }
  287. enum PlistKind {
  288. Path(PathBuf),
  289. Plist(plist::Value),
  290. }
  291. impl From<PathBuf> for PlistKind {
  292. fn from(p: PathBuf) -> Self {
  293. Self::Path(p)
  294. }
  295. }
  296. impl From<plist::Value> for PlistKind {
  297. fn from(p: plist::Value) -> Self {
  298. Self::Plist(p)
  299. }
  300. }
  301. fn merge_plist(src: Vec<PlistKind>, dest: &Path) -> Result<()> {
  302. let mut dest_plist = None;
  303. for plist_kind in src {
  304. let plist = match plist_kind {
  305. PlistKind::Path(p) => plist::Value::from_file(p),
  306. PlistKind::Plist(v) => Ok(v),
  307. };
  308. if let Ok(src_plist) = plist {
  309. if dest_plist.is_none() {
  310. dest_plist.replace(plist::Value::from_file(dest)?);
  311. }
  312. let plist = dest_plist.as_mut().expect("plist not loaded");
  313. if let Some(plist) = plist.as_dictionary_mut() {
  314. if let Some(dict) = src_plist.into_dictionary() {
  315. for (key, value) in dict {
  316. plist.insert(key, value);
  317. }
  318. }
  319. }
  320. }
  321. }
  322. if let Some(dest_plist) = dest_plist {
  323. dest_plist.to_file_xml(dest)?;
  324. }
  325. Ok(())
  326. }
  327. pub fn signing_from_env() -> Result<(
  328. Option<tauri_macos_sign::Keychain>,
  329. Option<tauri_macos_sign::ProvisioningProfile>,
  330. )> {
  331. let keychain = if let (Some(certificate), Some(certificate_password)) = (
  332. var_os("IOS_CERTIFICATE"),
  333. var_os("IOS_CERTIFICATE_PASSWORD"),
  334. ) {
  335. tauri_macos_sign::Keychain::with_certificate(&certificate, &certificate_password).map(Some)?
  336. } else {
  337. None
  338. };
  339. let provisioning_profile = if let Some(provisioning_profile) = var_os("IOS_MOBILE_PROVISION") {
  340. tauri_macos_sign::ProvisioningProfile::from_base64(&provisioning_profile).map(Some)?
  341. } else {
  342. None
  343. };
  344. Ok((keychain, provisioning_profile))
  345. }
  346. pub fn init_config(
  347. keychain: Option<&tauri_macos_sign::Keychain>,
  348. provisioning_profile: Option<&tauri_macos_sign::ProvisioningProfile>,
  349. ) -> Result<super::init::IosInitConfig> {
  350. Ok(super::init::IosInitConfig {
  351. code_sign_style: if keychain.is_some() && provisioning_profile.is_some() {
  352. super::init::CodeSignStyle::Manual
  353. } else {
  354. super::init::CodeSignStyle::Automatic
  355. },
  356. code_sign_identity: keychain.map(|k| k.signing_identity()),
  357. team_id: keychain.and_then(|k| k.team_id().map(ToString::to_string)),
  358. provisioning_profile_uuid: provisioning_profile.and_then(|p| p.uuid().ok()),
  359. })
  360. }