// Copyright 2019-2021 Tauri Programme within The Commons Conservancy // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT use cargo_mobile::{ apple::{ config::{ Config as AppleConfig, Metadata as AppleMetadata, Platform as ApplePlatform, Raw as RawAppleConfig, }, device::Device, ios_deploy, simctl, target::Target, teams::find_development_teams, }, config::app::App, env::Env, opts::NoiseLevel, os, util::prompt, }; use clap::{Parser, Subcommand}; use sublime_fuzzy::best_match; use super::{ ensure_init, env, get_app, init::{command as init_command, init_dot_cargo}, log_finished, read_options, CliOptions, Target as MobileTarget, MIN_DEVICE_MATCH_SCORE, }; use crate::{ helpers::config::{get as get_tauri_config, Config as TauriConfig}, Result, }; use std::{ process::exit, thread::{sleep, spawn}, time::Duration, }; mod build; mod dev; mod open; pub(crate) mod project; mod xcode_script; pub const APPLE_DEVELOPMENT_TEAM_ENV_VAR_NAME: &str = "TAURI_APPLE_DEVELOPMENT_TEAM"; #[derive(Parser)] #[clap( author, version, about = "iOS commands", subcommand_required(true), arg_required_else_help(true) )] pub struct Cli { #[clap(subcommand)] command: Commands, } #[derive(Debug, Parser)] #[clap(about = "Initializes a Tauri iOS project")] pub struct InitOptions { /// Skip prompting for values #[clap(long)] ci: bool, /// Reinstall dependencies #[clap(short, long)] reinstall_deps: bool, } #[derive(Subcommand)] enum Commands { Init(InitOptions), Open, Dev(dev::Options), Build(build::Options), #[clap(hide(true))] XcodeScript(xcode_script::Options), } pub fn command(cli: Cli, verbosity: u8) -> Result<()> { let noise_level = NoiseLevel::from_occurrences(verbosity as u64); match cli.command { Commands::Init(options) => init_command(MobileTarget::Ios, options.ci, options.reinstall_deps)?, Commands::Open => open::command()?, Commands::Dev(options) => dev::command(options, noise_level)?, Commands::Build(options) => build::command(options, noise_level)?, Commands::XcodeScript(options) => xcode_script::command(options)?, } Ok(()) } pub fn get_config( app: Option, config: &TauriConfig, cli_options: &CliOptions, ) -> (App, AppleConfig, AppleMetadata) { let app = app.unwrap_or_else(|| get_app(config)); let ios_options = cli_options.clone(); let raw = RawAppleConfig { development_team: std::env::var(APPLE_DEVELOPMENT_TEAM_ENV_VAR_NAME) .ok() .or_else(|| config.tauri.bundle.ios.development_team.clone()) .unwrap_or_else(|| { let teams = find_development_teams().unwrap_or_default(); match teams.len() { 0 => { 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`."); exit(1); } 1 => teams.first().unwrap().id.clone(), _ => { 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::>().join(", ")); exit(1); } } }), ios_features: ios_options.features.clone(), bundle_version: config.package.version.clone(), bundle_version_short: config.package.version.clone(), ..Default::default() }; let config = AppleConfig::from_raw(app.clone(), Some(raw)).unwrap(); let metadata = AppleMetadata { supported: true, ios: ApplePlatform { cargo_args: Some(ios_options.args), features: ios_options.features, ..Default::default() }, macos: Default::default(), }; (app, config, metadata) } fn with_config( cli_options: Option, f: impl FnOnce(&App, &AppleConfig, &AppleMetadata, CliOptions) -> Result, ) -> Result { let (app, config, metadata, cli_options) = { let tauri_config = get_tauri_config(None)?; let tauri_config_guard = tauri_config.lock().unwrap(); let tauri_config_ = tauri_config_guard.as_ref().unwrap(); let cli_options = cli_options.unwrap_or_else(read_options); let (app, config, metadata) = get_config(None, tauri_config_, &cli_options); (app, config, metadata, cli_options) }; f(&app, &config, &metadata, cli_options) } fn ios_deploy_device_prompt<'a>(env: &'_ Env, target: Option<&str>) -> Result> { let device_list = ios_deploy::device_list(env) .map_err(|cause| anyhow::anyhow!("Failed to detect connected iOS devices: {cause}"))?; if !device_list.is_empty() { let device = if let Some(t) = target { let (device, score) = device_list .into_iter() .rev() .map(|d| { let score = best_match(t, d.name()).map_or(0, |m| m.score()); (d, score) }) .max_by_key(|(_, score)| *score) // we already checked the list is not empty .unwrap(); if score > MIN_DEVICE_MATCH_SCORE { device } else { anyhow::bail!("Could not find an iOS device matching {t}") } } else { let index = if device_list.len() > 1 { prompt::list( concat!("Detected ", "iOS", " devices"), device_list.iter(), "device", None, "Device", ) .map_err(|cause| anyhow::anyhow!("Failed to prompt for iOS device: {cause}"))? } else { 0 }; device_list.into_iter().nth(index).unwrap() }; println!( "Detected connected device: {} with target {:?}", device, device.target().triple, ); Ok(device) } else { Err(anyhow::anyhow!("No connected iOS devices detected")) } } fn simulator_prompt(env: &'_ Env, target: Option<&str>) -> Result { let simulator_list = simctl::device_list(env).map_err(|cause| { anyhow::anyhow!("Failed to detect connected iOS Simulator devices: {cause}") })?; if !simulator_list.is_empty() { let device = if let Some(t) = target { let (device, score) = simulator_list .into_iter() .rev() .map(|d| { let score = best_match(t, d.name()).map_or(0, |m| m.score()); (d, score) }) .max_by_key(|(_, score)| *score) // we already checked the list is not empty .unwrap(); if score > MIN_DEVICE_MATCH_SCORE { device } else { anyhow::bail!("Could not find an iOS Simulator matching {t}") } } else if simulator_list.len() > 1 { let index = prompt::list( concat!("Detected ", "iOS", " simulators"), simulator_list.iter(), "simulator", None, "Simulator", ) .map_err(|cause| anyhow::anyhow!("Failed to prompt for iOS Simulator device: {cause}"))?; simulator_list.into_iter().nth(index).unwrap() } else { simulator_list.into_iter().next().unwrap() }; log::info!("Starting simulator {}", device.name()); let handle = device.start(env)?; spawn(move || { let _ = handle.wait(); }); Ok(device) } else { Err(anyhow::anyhow!("No available iOS Simulator detected")) } } fn device_prompt<'a>(env: &'_ Env, target: Option<&str>) -> Result> { if let Ok(device) = ios_deploy_device_prompt(env, target) { Ok(device) } else { let simulator = simulator_prompt(env, target)?; let handle = simulator.start(env)?; spawn(move || { let _ = handle.wait(); }); Ok(simulator.into()) } } fn detect_target_ok<'a>(env: &Env) -> Option<&'a Target<'a>> { device_prompt(env, None).map(|device| device.target()).ok() } fn open_and_wait(config: &AppleConfig, env: &Env) -> ! { log::info!("Opening Xcode"); if let Err(e) = os::open_file_with("Xcode", config.project_dir(), env) { log::error!("{}", e); } loop { sleep(Duration::from_secs(24 * 60 * 60)); } }