123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451 |
- // Copyright 2019-2024 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, get_app, get_config, inject_assets,
- merge_plist, open_and_wait, MobileTarget,
- };
- use crate::{
- dev::Options as DevOptions,
- helpers::{
- app_paths::tauri_dir,
- config::{get as get_tauri_config, reload as reload_config, ConfigHandle},
- flock,
- },
- interface::{AppInterface, AppSettings, Interface, MobileOptions, Options as InterfaceOptions},
- mobile::{write_options, CliOptions, DevChild, DevProcess},
- ConfigValue, Result,
- };
- use clap::{ArgAction, Parser};
- use anyhow::Context;
- use cargo_mobile2::{
- apple::{
- config::Config as AppleConfig,
- device::{Device, DeviceKind},
- },
- config::app::App,
- env::Env,
- opts::{NoiseLevel, Profile},
- };
- use std::{
- env::set_current_dir,
- net::{IpAddr, Ipv4Addr, SocketAddr},
- sync::OnceLock,
- };
- 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 {
- /// List of cargo features to activate
- #[clap(short, long, action = ArgAction::Append, num_args(0..))]
- pub features: Option<Vec<String>>,
- /// 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<ConfigValue>,
- /// Run the code in release mode
- #[clap(long = "release")]
- pub release_mode: bool,
- /// Skip waiting for the frontend dev server to start before building the tauri application.
- #[clap(long, env = "TAURI_CLI_NO_DEV_SERVER_WAIT")]
- pub no_dev_server_wait: bool,
- /// Disable the file watcher
- #[clap(long)]
- pub no_watch: 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<String>,
- /// Force prompting for an IP to use to connect to the dev server on mobile.
- #[clap(long)]
- pub force_ip_prompt: bool,
- /// Use the public network address for the development server.
- /// If an actual address it provided, it is used instead of prompting to pick one.
- ///
- /// This option is particularly useful along the `--open` flag when you intend on running on a physical device.
- ///
- /// This replaces the devUrl configuration value to match the public network address host,
- /// it is your responsability to set up your development server to listen on this address
- /// by using 0.0.0.0 as host for instance.
- ///
- /// When this is set or when running on an iOS device the CLI sets the `TAURI_DEV_HOST`
- /// environment variable so you can check this on your framework's configuration to expose the development server
- /// on the public network address.
- #[clap(long)]
- pub host: Option<Option<IpAddr>>,
- /// Disable the built-in dev server for static files.
- #[clap(long)]
- pub no_dev_server: bool,
- /// Specify port for the built-in dev server for static files. Defaults to 1430.
- #[clap(long, env = "TAURI_CLI_PORT")]
- pub port: Option<u16>,
- }
- 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: Vec::new(),
- no_watch: options.no_watch,
- no_dev_server: options.no_dev_server,
- no_dev_server_wait: options.no_dev_server_wait,
- port: options.port,
- host: None,
- }
- }
- }
- pub fn command(options: Options, noise_level: NoiseLevel) -> Result<()> {
- crate::helpers::app_paths::resolve();
- let result = run_command(options, noise_level);
- if result.is_err() {
- crate::dev::kill_before_dev_process();
- }
- result
- }
- fn run_command(options: Options, noise_level: NoiseLevel) -> Result<()> {
- 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();
- 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());
- let tauri_config = get_tauri_config(
- tauri_utils::platform::Target::Ios,
- options.config.as_ref().map(|c| &c.0),
- )?;
- let (interface, app, config) = {
- let tauri_config_guard = tauri_config.lock().unwrap();
- let tauri_config_ = tauri_config_guard.as_ref().unwrap();
- let interface = AppInterface::new(tauri_config_, Some(target_triple))?;
- let app = get_app(tauri_config_, &interface);
- let (config, _metadata) = get_config(
- &app,
- tauri_config_,
- dev_options.features.as_ref(),
- &Default::default(),
- );
- (interface, app, config)
- };
- let tauri_path = tauri_dir();
- set_current_dir(tauri_path).with_context(|| "failed to change current working directory")?;
- ensure_init(
- &tauri_config,
- config.app(),
- config.project_dir(),
- MobileTarget::Ios,
- )?;
- inject_assets(&config)?;
- let info_plist_path = config
- .project_dir()
- .join(config.scheme())
- .join("Info.plist");
- merge_plist(
- vec![
- tauri_path.join("Info.plist").into(),
- tauri_path.join("Info.ios.plist").into(),
- ],
- &info_plist_path,
- )?;
- run_dev(
- interface,
- options,
- dev_options,
- tauri_config,
- device,
- env,
- &app,
- &config,
- noise_level,
- )
- }
- fn local_ip_address(force: bool) -> &'static IpAddr {
- static LOCAL_IP: OnceLock<IpAddr> = OnceLock::new();
- LOCAL_IP.get_or_init(|| {
- let prompt_for_ip = || {
- let addresses: Vec<IpAddr> = local_ip_address::list_afinet_netifas()
- .expect("failed to list networks")
- .into_iter()
- .map(|(_, ipaddr)| ipaddr)
- .filter(|ipaddr| match ipaddr {
- IpAddr::V4(i) => i != &Ipv4Addr::LOCALHOST,
- IpAddr::V6(i) => i.to_string().ends_with("::2"),
- })
- .collect();
- match addresses.len() {
- 0 => panic!("No external IP detected."),
- 1 => {
- let ipaddr = addresses.first().unwrap();
- *ipaddr
- }
- _ => {
- let selected = dialoguer::Select::with_theme(&dialoguer::theme::ColorfulTheme::default())
- .with_prompt(
- "Failed to detect external IP, What IP should we use to access your development server?",
- )
- .items(&addresses)
- .default(0)
- .interact()
- .expect("failed to select external IP");
- *addresses.get(selected).unwrap()
- }
- }
- };
- let ip = if force {
- prompt_for_ip()
- } else {
- local_ip_address::local_ip().unwrap_or_else(|_| prompt_for_ip())
- };
- log::info!("Using {ip} to access the development server.");
- ip
- })
- }
- fn use_network_address_for_dev_url(
- config: &ConfigHandle,
- options: &mut Options,
- dev_options: &mut DevOptions,
- ) -> crate::Result<()> {
- let mut dev_url = config
- .lock()
- .unwrap()
- .as_ref()
- .unwrap()
- .build
- .dev_url
- .clone();
- let ip = if let Some(url) = &mut dev_url {
- let localhost = match url.host() {
- Some(url::Host::Domain(d)) => d == "localhost",
- Some(url::Host::Ipv4(i)) => {
- i == std::net::Ipv4Addr::LOCALHOST || i == std::net::Ipv4Addr::UNSPECIFIED
- }
- _ => false,
- };
- if localhost {
- let ip = options
- .host
- .unwrap_or_default()
- .unwrap_or_else(|| *local_ip_address(options.force_ip_prompt));
- log::info!(
- "Replacing devUrl host with {ip}. {}.",
- "If your frontend is not listening on that address, try configuring your development server to use the `TAURI_DEV_HOST` environment variable or 0.0.0.0 as host"
- );
- *url = url::Url::parse(&format!(
- "{}://{}{}",
- url.scheme(),
- SocketAddr::new(ip, url.port_or_known_default().unwrap()),
- url.path()
- ))?;
- if let Some(c) = &mut options.config {
- if let Some(build) = c
- .0
- .as_object_mut()
- .and_then(|root| root.get_mut("build"))
- .and_then(|build| build.as_object_mut())
- {
- build.insert("devUrl".into(), url.to_string().into());
- }
- } else {
- let mut build = serde_json::Map::new();
- build.insert("devUrl".into(), url.to_string().into());
- options
- .config
- .replace(crate::ConfigValue(serde_json::json!({
- "build": build
- })));
- }
- reload_config(options.config.as_ref().map(|c| &c.0))?;
- Some(ip)
- } else {
- None
- }
- } else if !dev_options.no_dev_server {
- let ip = options
- .host
- .unwrap_or_default()
- .unwrap_or_else(|| *local_ip_address(options.force_ip_prompt));
- dev_options.host.replace(ip);
- Some(ip)
- } else {
- None
- };
- if let Some(ip) = ip {
- std::env::set_var("TAURI_DEV_HOST", ip.to_string());
- if ip.is_ipv6() {
- // in this case we can't ping the server for some reason
- dev_options.no_dev_server_wait = true;
- }
- }
- Ok(())
- }
- #[allow(clippy::too_many_arguments)]
- fn run_dev(
- mut interface: AppInterface,
- mut options: Options,
- mut dev_options: DevOptions,
- tauri_config: ConfigHandle,
- device: Option<Device>,
- env: Env,
- app: &App,
- config: &AppleConfig,
- noise_level: NoiseLevel,
- ) -> Result<()> {
- // when running on an actual device we must use the network IP
- if options.host.is_some()
- || device
- .as_ref()
- .map(|device| !matches!(device.kind(), DeviceKind::Simulator))
- .unwrap_or(false)
- {
- use_network_address_for_dev_url(&tauri_config, &mut options, &mut dev_options)?;
- }
- crate::dev::setup(&interface, &mut dev_options, tauri_config.clone())?;
- 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")?;
- let set_host = options.host.is_some();
- 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: dev_options.config.clone(),
- no_watch: options.no_watch,
- },
- |options| {
- let cli_options = CliOptions {
- dev: true,
- features: options.features.clone(),
- args: options.args.clone(),
- noise_level,
- vars: Default::default(),
- config: dev_options.config.clone(),
- };
- let _handle = write_options(
- &tauri_config.lock().unwrap().as_ref().unwrap().identifier,
- cli_options,
- )?;
- if open {
- if !set_host {
- log::warn!("{PHYSICAL_IPHONE_DEV_WARNING}");
- }
- 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<dyn DevProcess + Send>)
- }
- Err(e) => {
- crate::dev::kill_before_dev_process();
- Err(e)
- }
- }
- } else {
- if !set_host {
- log::warn!("{PHYSICAL_IPHONE_DEV_WARNING}");
- }
- open_and_wait(config, &env)
- }
- },
- )
- }
- fn run(
- device: &Device<'_>,
- options: MobileOptions,
- config: &AppleConfig,
- env: &Env,
- ) -> crate::Result<DevChild> {
- 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(Into::into)
- }
|