mod.rs 11 KB

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