use super::{
device_prompt, ensure_init, env, get_app, get_config, inject_resources, load_pbxproj,
open_and_wait, synchronize_project_config, MobileTarget, ProjectConfig,
};
use crate::{
dev::Options as DevOptions,
error::{Context, ErrorExt},
helpers::{
app_paths::Dirs,
config::{get_config as get_tauri_config, ConfigMetadata},
flock,
plist::merge_plist,
},
interface::{AppInterface, MobileOptions, Options as InterfaceOptions},
mobile::{
ios::ensure_ios_runtime_installed, use_network_address_for_dev_url, write_options, CliOptions,
DevChild, DevHost, DevProcess,
},
ConfigValue, Result,
};
use clap::{ArgAction, Parser};
use cargo_mobile2::{
apple::{
config::Config as AppleConfig,
device::{Device, DeviceKind, RunError},
target::BuildError,
},
env::Env,
opts::{NoiseLevel, Profile},
};
use url::Host;
use std::{env::set_current_dir, net::Ipv4Addr, path::PathBuf};
const PHYSICAL_IPHONE_DEV_WARNING: &str = "To develop on physical phones you need the `--host` option (not required for Simulators). See the documentation for more information: https://v2.tauri.app/develop/#development-server";
#[derive(Debug, Clone, Parser)]
#[clap(
about = "Run your app in development mode on iOS",
long_about = "Run your app in development mode on iOS with hot-reloading for the Rust code.
It makes use of the `build.devUrl` property from your `tauri.conf.json` file.
It also runs your `build.beforeDevCommand` which usually starts your frontend devServer.
When connected to a physical iOS device, the public network address must be used instead of `localhost`
for the devUrl property. Tauri makes that change automatically, but your dev server might need
a different configuration to listen on the public address. You can check the `TAURI_DEV_HOST`
environment variable to determine whether the public network should be used or not."
)]
pub struct Options {
#[clap(short, long, action = ArgAction::Append, num_args(0..), value_delimiter = ',')]
pub features: Vec<String>,
#[clap(short, long)]
exit_on_panic: bool,
#[clap(short, long)]
pub config: Vec<ConfigValue>,
#[clap(long = "release")]
pub release_mode: bool,
#[clap(long, env = "TAURI_CLI_NO_DEV_SERVER_WAIT")]
pub no_dev_server_wait: bool,
#[clap(long)]
pub no_watch: bool,
#[clap(long)]
pub additional_watch_folders: Vec<PathBuf>,
#[clap(short, long)]
pub open: bool,
pub device: Option<String>,
#[clap(long)]
pub force_ip_prompt: bool,
#[clap(long, default_value_t, default_missing_value(""), num_args(0..=1))]
pub host: DevHost,
#[clap(long)]
pub no_dev_server: bool,
#[clap(long, env = "TAURI_CLI_PORT")]
pub port: Option<u16>,
#[clap(last(true))]
pub args: Vec<String>,
#[clap(long, env = "TAURI_DEV_ROOT_CERTIFICATE_PATH")]
pub root_certificate_path: Option<PathBuf>,
}
impl From<Options> 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: options.args,
no_watch: options.no_watch,
additional_watch_folders: options.additional_watch_folders,
no_dev_server: options.no_dev_server,
no_dev_server_wait: options.no_dev_server_wait,
port: options.port,
host: options.host.0.unwrap_or_default(),
}
}
}
pub fn command(options: Options, noise_level: NoiseLevel) -> Result<()> {
let dirs = crate::helpers::app_paths::resolve_dirs();
let result = run_command(options, noise_level, dirs);
if result.is_err() {
crate::dev::kill_before_dev_process();
}
result
}
fn run_command(options: Options, noise_level: NoiseLevel, dirs: Dirs) -> Result<()> {
if let Some(root_certificate_path) = &options.root_certificate_path {
std::env::set_var(
"TAURI_DEV_ROOT_CERTIFICATE",
std::fs::read_to_string(root_certificate_path).fs_context(
"failed to read root certificate file",
root_certificate_path.clone(),
)?,
);
}
let env = env().context("failed to load iOS environment")?;
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
}
}
};
if device.is_some() {
ensure_ios_runtime_installed()?;
}
let mut dev_options: DevOptions = options.clone().into();
let target_triple = device
.as_ref()
.map(|d| d.target().triple.to_string())
.unwrap_or_else(|| "aarch64-apple-ios".into());
dev_options.target = Some(target_triple.clone());
dev_options.args.push("--lib".into());
let tauri_config = get_tauri_config(
tauri_utils::platform::Target::Ios,
&options.config.iter().map(|c| &c.0).collect::<Vec<_>>(),
dirs.tauri,
)?;
let interface = AppInterface::new(&tauri_config, Some(target_triple), dirs.tauri)?;
let app = get_app(MobileTarget::Ios, &tauri_config, &interface, dirs.tauri);
let (config, _) = get_config(
&app,
&tauri_config,
&dev_options.features,
&CliOptions {
dev: true,
features: dev_options.features.clone(),
args: dev_options.args.clone(),
noise_level,
vars: Default::default(),
config: dev_options.config.clone(),
target_device: None,
},
dirs.tauri,
)?;
set_current_dir(dirs.tauri).context("failed to set current directory to Tauri directory")?;
ensure_init(
&tauri_config,
config.app(),
config.project_dir(),
MobileTarget::Ios,
false,
)?;
inject_resources(&config, &tauri_config)?;
let info_plist_path = config
.project_dir()
.join(config.scheme())
.join("Info.plist");
let mut src_plists = vec![info_plist_path.clone().into()];
if dirs.tauri.join("Info.plist").exists() {
src_plists.push(dirs.tauri.join("Info.plist").into());
}
if dirs.tauri.join("Info.ios.plist").exists() {
src_plists.push(dirs.tauri.join("Info.ios.plist").into());
}
if let Some(info_plist) = &tauri_config.bundle.ios.info_plist {
src_plists.push(info_plist.clone().into());
}
let merged_info_plist = merge_plist(src_plists)?;
merged_info_plist
.to_file_xml(&info_plist_path)
.map_err(std::io::Error::other)
.fs_context("failed to save merged Info.plist file", info_plist_path)?;
let mut pbxproj = load_pbxproj(&config)?;
synchronize_project_config(
&config,
&tauri_config,
&mut pbxproj,
&mut plist::Dictionary::new(),
&ProjectConfig {
code_sign_identity: None,
team_id: None,
provisioning_profile_uuid: None,
},
!options.release_mode,
)?;
if pbxproj.has_changes() {
pbxproj
.save()
.fs_context("failed to save pbxproj file", pbxproj.path)?;
}
run_dev(
interface,
options,
dev_options,
tauri_config,
device,
env,
&config,
noise_level,
&dirs,
)
}
#[allow(clippy::too_many_arguments)]
fn run_dev(
mut interface: AppInterface,
options: Options,
mut dev_options: DevOptions,
mut tauri_config: ConfigMetadata,
device: Option<Device>,
env: Env,
config: &AppleConfig,
noise_level: NoiseLevel,
dirs: &Dirs,
) -> Result<()> {
if options.host.0.is_some()
|| device
.as_ref()
.map(|device| !matches!(device.kind(), DeviceKind::Simulator))
.unwrap_or(false)
|| tauri_config.build.dev_url.as_ref().is_some_and(|url| {
matches!(
url.host(),
Some(Host::Ipv4(i)) if i == Ipv4Addr::UNSPECIFIED
)
})
{
use_network_address_for_dev_url(
&mut tauri_config,
&mut dev_options,
options.force_ip_prompt,
dirs.tauri,
)?;
}
crate::dev::setup(&interface, &mut dev_options, &mut tauri_config, dirs)?;
let app_settings = interface.app_settings();
let out_dir = app_settings.out_dir(
&InterfaceOptions {
debug: !dev_options.release_mode,
target: dev_options.target.clone(),
..Default::default()
},
dirs.tauri,
)?;
let _lock = flock::open_rw(out_dir.join("lock").with_extension("ios"), "iOS")?;
let set_host = options.host.0.is_some();
let open = options.open;
interface.mobile_dev(
&mut tauri_config,
MobileOptions {
debug: true,
features: options.features,
args: options.args,
config: dev_options.config.clone(),
no_watch: options.no_watch,
additional_watch_folders: options.additional_watch_folders,
},
|options, tauri_config| {
let cli_options = CliOptions {
dev: true,
features: options.features.clone(),
args: options.args.clone(),
noise_level,
vars: Default::default(),
config: dev_options.config.clone(),
target_device: None,
};
let _handle = write_options(tauri_config, cli_options)?;
let open_xcode = || {
if !set_host {
log::warn!("{PHYSICAL_IPHONE_DEV_WARNING}");
}
open_and_wait(config, &env)
};
if open {
open_xcode()
} else if let Some(device) = &device {
match run(device, options, config, noise_level, &env) {
Ok(c) => Ok(Box::new(c) as Box<dyn DevProcess + Send>),
Err(RunError::BuildFailed(BuildError::Sdk(sdk_err))) => {
log::warn!("{sdk_err}");
open_xcode()
}
Err(e) => {
crate::dev::kill_before_dev_process();
crate::error::bail!("failed to run iOS app: {}", e)
}
}
} else {
open_xcode()
}
},
dirs,
)
}
fn run(
device: &Device<'_>,
options: MobileOptions,
config: &AppleConfig,
noise_level: NoiseLevel,
env: &Env,
) -> std::result::Result<DevChild, RunError> {
let profile = if options.debug {
Profile::Debug
} else {
Profile::Release
};
device
.run(
config,
env,
noise_level,
false, profile,
)
.map(DevChild::new)
}