// Copyright 2019-2023 Tauri Programme within The Commons Conservancy // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT use super::{ configure_cargo, device_prompt, ensure_init, env, open_and_wait, setup_dev_config, with_config, MobileTarget, APPLE_DEVELOPMENT_TEAM_ENV_VAR_NAME, }; use crate::{ dev::Options as DevOptions, helpers::{config::get as get_config, flock}, interface::{AppSettings, Interface, MobileOptions, Options as InterfaceOptions}, mobile::{write_options, CliOptions, DevChild, DevProcess}, Result, }; use clap::{ArgAction, Parser}; use dialoguer::{theme::ColorfulTheme, Select}; use tauri_mobile::{ apple::{config::Config as AppleConfig, device::Device, teams::find_development_teams}, config::app::App, env::Env, opts::{NoiseLevel, Profile}, }; use std::env::{set_var, var_os}; #[derive(Debug, Clone, Parser)] #[clap(about = "iOS dev")] pub struct Options { /// List of cargo features to activate #[clap(short, long, action = ArgAction::Append, num_args(0..))] pub features: Option>, /// Exit on panic #[clap(short, long)] exit_on_panic: bool, /// JSON string or path to JSON file to merge with tauri.conf.json #[clap(short, long)] pub config: Option, /// Run the code in release mode #[clap(long = "release")] pub release_mode: bool, /// Disable the file watcher #[clap(long)] pub no_watch: bool, /// Disable the dev server for static files. #[clap(long)] pub no_dev_server: bool, /// Open Xcode instead of trying to run on a connected device #[clap(short, long)] pub open: bool, /// Runs on the given device name pub device: Option, /// Force prompting for an IP to use to connect to the dev server on mobile. #[clap(long)] pub force_ip_prompt: bool, } impl From for DevOptions { fn from(options: Options) -> Self { Self { runner: None, target: None, features: options.features, exit_on_panic: options.exit_on_panic, config: options.config, release_mode: options.release_mode, args: Vec::new(), no_watch: options.no_watch, no_dev_server: options.no_dev_server, force_ip_prompt: options.force_ip_prompt, } } } pub fn command(options: Options, noise_level: NoiseLevel) -> Result<()> { if var_os(APPLE_DEVELOPMENT_TEAM_ENV_VAR_NAME).is_none() { if let Ok(teams) = find_development_teams() { let index = match teams.len() { 0 => None, 1 => Some(0), _ => { let index = Select::with_theme(&ColorfulTheme::default()) .items( &teams .iter() .map(|t| format!("{} (ID: {})", t.name, t.id)) .collect::>(), ) .default(0) .interact()?; Some(index) } }; if let Some(index) = index { let team = teams.get(index).unwrap(); log::info!( "Using development team `{}`. To make this permanent, set the `{}` environment variable to `{}`", team.name, APPLE_DEVELOPMENT_TEAM_ENV_VAR_NAME, team.id ); set_var(APPLE_DEVELOPMENT_TEAM_ENV_VAR_NAME, &team.id); } } } with_config( Some(Default::default()), |app, config, _metadata, _cli_options| { ensure_init(config.project_dir(), MobileTarget::Ios)?; run_dev(options, app, config, noise_level).map_err(Into::into) }, ) } fn run_dev( mut options: Options, app: &App, config: &AppleConfig, noise_level: NoiseLevel, ) -> Result<()> { setup_dev_config(&mut options.config, options.force_ip_prompt)?; let env = env()?; let device = if options.open { None } else { match device_prompt(&env, options.device.as_deref()) { Ok(d) => Some(d), Err(e) => { log::error!("{e}"); None } } }; let mut dev_options: DevOptions = options.clone().into(); dev_options.target = Some( device .as_ref() .map(|d| d.target().triple.to_string()) .unwrap_or_else(|| "aarch64-apple-ios".into()), ); let mut interface = crate::dev::setup(&mut dev_options, true)?; let app_settings = interface.app_settings(); let bin_path = app_settings.app_binary_path(&InterfaceOptions { debug: !dev_options.release_mode, target: dev_options.target.clone(), ..Default::default() })?; let out_dir = bin_path.parent().unwrap(); let _lock = flock::open_rw(out_dir.join("lock").with_extension("ios"), "iOS")?; configure_cargo(app, None)?; let open = options.open; let exit_on_panic = options.exit_on_panic; let no_watch = options.no_watch; interface.mobile_dev( MobileOptions { debug: true, features: options.features, args: Vec::new(), config: options.config, no_watch: options.no_watch, }, |options| { let cli_options = CliOptions { features: options.features.clone(), args: options.args.clone(), noise_level, vars: Default::default(), }; let _handle = write_options( &get_config(options.config.as_deref())? .lock() .unwrap() .as_ref() .unwrap() .tauri .bundle .identifier, cli_options, )?; if open { open_and_wait(config, &env) } else if let Some(device) = &device { match run(device, options, config, &env) { Ok(c) => { crate::dev::wait_dev_process(c.clone(), move |status, reason| { crate::dev::on_app_exit(status, reason, exit_on_panic, no_watch) }); Ok(Box::new(c) as Box) } Err(e) => { crate::dev::kill_before_dev_process(); Err(e.into()) } } } else { open_and_wait(config, &env) } }, ) } #[derive(Debug, thiserror::Error)] enum RunError { #[error("{0}")] RunFailed(String), } fn run( device: &Device<'_>, options: MobileOptions, config: &AppleConfig, env: &Env, ) -> Result { let profile = if options.debug { Profile::Debug } else { Profile::Release }; device .run( config, env, NoiseLevel::FranklyQuitePedantic, false, // do not quit on app exit profile, ) .map(DevChild::new) .map_err(|e| RunError::RunFailed(e.to_string())) }