dev.rs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451
  1. // Copyright 2019-2024 Tauri Programme within The Commons Conservancy
  2. // SPDX-License-Identifier: Apache-2.0
  3. // SPDX-License-Identifier: MIT
  4. use super::{
  5. configure_cargo, device_prompt, ensure_init, env, get_app, get_config, inject_assets,
  6. merge_plist, open_and_wait, MobileTarget,
  7. };
  8. use crate::{
  9. dev::Options as DevOptions,
  10. helpers::{
  11. app_paths::tauri_dir,
  12. config::{get as get_tauri_config, reload as reload_config, ConfigHandle},
  13. flock,
  14. },
  15. interface::{AppInterface, AppSettings, Interface, MobileOptions, Options as InterfaceOptions},
  16. mobile::{write_options, CliOptions, DevChild, DevProcess},
  17. ConfigValue, Result,
  18. };
  19. use clap::{ArgAction, Parser};
  20. use anyhow::Context;
  21. use cargo_mobile2::{
  22. apple::{
  23. config::Config as AppleConfig,
  24. device::{Device, DeviceKind},
  25. },
  26. config::app::App,
  27. env::Env,
  28. opts::{NoiseLevel, Profile},
  29. };
  30. use std::{
  31. env::set_current_dir,
  32. net::{IpAddr, Ipv4Addr, SocketAddr},
  33. sync::OnceLock,
  34. };
  35. 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";
  36. #[derive(Debug, Clone, Parser)]
  37. #[clap(
  38. about = "Run your app in development mode on iOS",
  39. long_about = "Run your app in development mode on iOS with hot-reloading for the Rust code.
  40. It makes use of the `build.devUrl` property from your `tauri.conf.json` file.
  41. It also runs your `build.beforeDevCommand` which usually starts your frontend devServer.
  42. When connected to a physical iOS device, the public network address must be used instead of `localhost`
  43. for the devUrl property. Tauri makes that change automatically, but your dev server might need
  44. a different configuration to listen on the public address. You can check the `TAURI_DEV_HOST`
  45. environment variable to determine whether the public network should be used or not."
  46. )]
  47. pub struct Options {
  48. /// List of cargo features to activate
  49. #[clap(short, long, action = ArgAction::Append, num_args(0..))]
  50. pub features: Option<Vec<String>>,
  51. /// Exit on panic
  52. #[clap(short, long)]
  53. exit_on_panic: bool,
  54. /// JSON string or path to JSON file to merge with tauri.conf.json
  55. #[clap(short, long)]
  56. pub config: Option<ConfigValue>,
  57. /// Run the code in release mode
  58. #[clap(long = "release")]
  59. pub release_mode: bool,
  60. /// Skip waiting for the frontend dev server to start before building the tauri application.
  61. #[clap(long, env = "TAURI_CLI_NO_DEV_SERVER_WAIT")]
  62. pub no_dev_server_wait: bool,
  63. /// Disable the file watcher
  64. #[clap(long)]
  65. pub no_watch: bool,
  66. /// Open Xcode instead of trying to run on a connected device
  67. #[clap(short, long)]
  68. pub open: bool,
  69. /// Runs on the given device name
  70. pub device: Option<String>,
  71. /// Force prompting for an IP to use to connect to the dev server on mobile.
  72. #[clap(long)]
  73. pub force_ip_prompt: bool,
  74. /// Use the public network address for the development server.
  75. /// If an actual address it provided, it is used instead of prompting to pick one.
  76. ///
  77. /// This option is particularly useful along the `--open` flag when you intend on running on a physical device.
  78. ///
  79. /// This replaces the devUrl configuration value to match the public network address host,
  80. /// it is your responsability to set up your development server to listen on this address
  81. /// by using 0.0.0.0 as host for instance.
  82. ///
  83. /// When this is set or when running on an iOS device the CLI sets the `TAURI_DEV_HOST`
  84. /// environment variable so you can check this on your framework's configuration to expose the development server
  85. /// on the public network address.
  86. #[clap(long)]
  87. pub host: Option<Option<IpAddr>>,
  88. /// Disable the built-in dev server for static files.
  89. #[clap(long)]
  90. pub no_dev_server: bool,
  91. /// Specify port for the built-in dev server for static files. Defaults to 1430.
  92. #[clap(long, env = "TAURI_CLI_PORT")]
  93. pub port: Option<u16>,
  94. }
  95. impl From<Options> for DevOptions {
  96. fn from(options: Options) -> Self {
  97. Self {
  98. runner: None,
  99. target: None,
  100. features: options.features,
  101. exit_on_panic: options.exit_on_panic,
  102. config: options.config,
  103. release_mode: options.release_mode,
  104. args: Vec::new(),
  105. no_watch: options.no_watch,
  106. no_dev_server: options.no_dev_server,
  107. no_dev_server_wait: options.no_dev_server_wait,
  108. port: options.port,
  109. host: None,
  110. }
  111. }
  112. }
  113. pub fn command(options: Options, noise_level: NoiseLevel) -> Result<()> {
  114. crate::helpers::app_paths::resolve();
  115. let result = run_command(options, noise_level);
  116. if result.is_err() {
  117. crate::dev::kill_before_dev_process();
  118. }
  119. result
  120. }
  121. fn run_command(options: Options, noise_level: NoiseLevel) -> Result<()> {
  122. let env = env()?;
  123. let device = if options.open {
  124. None
  125. } else {
  126. match device_prompt(&env, options.device.as_deref()) {
  127. Ok(d) => Some(d),
  128. Err(e) => {
  129. log::error!("{e}");
  130. None
  131. }
  132. }
  133. };
  134. let mut dev_options: DevOptions = options.clone().into();
  135. let target_triple = device
  136. .as_ref()
  137. .map(|d| d.target().triple.to_string())
  138. .unwrap_or_else(|| "aarch64-apple-ios".into());
  139. dev_options.target = Some(target_triple.clone());
  140. let tauri_config = get_tauri_config(
  141. tauri_utils::platform::Target::Ios,
  142. options.config.as_ref().map(|c| &c.0),
  143. )?;
  144. let (interface, app, config) = {
  145. let tauri_config_guard = tauri_config.lock().unwrap();
  146. let tauri_config_ = tauri_config_guard.as_ref().unwrap();
  147. let interface = AppInterface::new(tauri_config_, Some(target_triple))?;
  148. let app = get_app(tauri_config_, &interface);
  149. let (config, _metadata) = get_config(
  150. &app,
  151. tauri_config_,
  152. dev_options.features.as_ref(),
  153. &Default::default(),
  154. );
  155. (interface, app, config)
  156. };
  157. let tauri_path = tauri_dir();
  158. set_current_dir(tauri_path).with_context(|| "failed to change current working directory")?;
  159. ensure_init(
  160. &tauri_config,
  161. config.app(),
  162. config.project_dir(),
  163. MobileTarget::Ios,
  164. )?;
  165. inject_assets(&config)?;
  166. let info_plist_path = config
  167. .project_dir()
  168. .join(config.scheme())
  169. .join("Info.plist");
  170. merge_plist(
  171. vec![
  172. tauri_path.join("Info.plist").into(),
  173. tauri_path.join("Info.ios.plist").into(),
  174. ],
  175. &info_plist_path,
  176. )?;
  177. run_dev(
  178. interface,
  179. options,
  180. dev_options,
  181. tauri_config,
  182. device,
  183. env,
  184. &app,
  185. &config,
  186. noise_level,
  187. )
  188. }
  189. fn local_ip_address(force: bool) -> &'static IpAddr {
  190. static LOCAL_IP: OnceLock<IpAddr> = OnceLock::new();
  191. LOCAL_IP.get_or_init(|| {
  192. let prompt_for_ip = || {
  193. let addresses: Vec<IpAddr> = local_ip_address::list_afinet_netifas()
  194. .expect("failed to list networks")
  195. .into_iter()
  196. .map(|(_, ipaddr)| ipaddr)
  197. .filter(|ipaddr| match ipaddr {
  198. IpAddr::V4(i) => i != &Ipv4Addr::LOCALHOST,
  199. IpAddr::V6(i) => i.to_string().ends_with("::2"),
  200. })
  201. .collect();
  202. match addresses.len() {
  203. 0 => panic!("No external IP detected."),
  204. 1 => {
  205. let ipaddr = addresses.first().unwrap();
  206. *ipaddr
  207. }
  208. _ => {
  209. let selected = dialoguer::Select::with_theme(&dialoguer::theme::ColorfulTheme::default())
  210. .with_prompt(
  211. "Failed to detect external IP, What IP should we use to access your development server?",
  212. )
  213. .items(&addresses)
  214. .default(0)
  215. .interact()
  216. .expect("failed to select external IP");
  217. *addresses.get(selected).unwrap()
  218. }
  219. }
  220. };
  221. let ip = if force {
  222. prompt_for_ip()
  223. } else {
  224. local_ip_address::local_ip().unwrap_or_else(|_| prompt_for_ip())
  225. };
  226. log::info!("Using {ip} to access the development server.");
  227. ip
  228. })
  229. }
  230. fn use_network_address_for_dev_url(
  231. config: &ConfigHandle,
  232. options: &mut Options,
  233. dev_options: &mut DevOptions,
  234. ) -> crate::Result<()> {
  235. let mut dev_url = config
  236. .lock()
  237. .unwrap()
  238. .as_ref()
  239. .unwrap()
  240. .build
  241. .dev_url
  242. .clone();
  243. let ip = if let Some(url) = &mut dev_url {
  244. let localhost = match url.host() {
  245. Some(url::Host::Domain(d)) => d == "localhost",
  246. Some(url::Host::Ipv4(i)) => {
  247. i == std::net::Ipv4Addr::LOCALHOST || i == std::net::Ipv4Addr::UNSPECIFIED
  248. }
  249. _ => false,
  250. };
  251. if localhost {
  252. let ip = options
  253. .host
  254. .unwrap_or_default()
  255. .unwrap_or_else(|| *local_ip_address(options.force_ip_prompt));
  256. log::info!(
  257. "Replacing devUrl host with {ip}. {}.",
  258. "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"
  259. );
  260. *url = url::Url::parse(&format!(
  261. "{}://{}{}",
  262. url.scheme(),
  263. SocketAddr::new(ip, url.port_or_known_default().unwrap()),
  264. url.path()
  265. ))?;
  266. if let Some(c) = &mut options.config {
  267. if let Some(build) = c
  268. .0
  269. .as_object_mut()
  270. .and_then(|root| root.get_mut("build"))
  271. .and_then(|build| build.as_object_mut())
  272. {
  273. build.insert("devUrl".into(), url.to_string().into());
  274. }
  275. } else {
  276. let mut build = serde_json::Map::new();
  277. build.insert("devUrl".into(), url.to_string().into());
  278. options
  279. .config
  280. .replace(crate::ConfigValue(serde_json::json!({
  281. "build": build
  282. })));
  283. }
  284. reload_config(options.config.as_ref().map(|c| &c.0))?;
  285. Some(ip)
  286. } else {
  287. None
  288. }
  289. } else if !dev_options.no_dev_server {
  290. let ip = options
  291. .host
  292. .unwrap_or_default()
  293. .unwrap_or_else(|| *local_ip_address(options.force_ip_prompt));
  294. dev_options.host.replace(ip);
  295. Some(ip)
  296. } else {
  297. None
  298. };
  299. if let Some(ip) = ip {
  300. std::env::set_var("TAURI_DEV_HOST", ip.to_string());
  301. if ip.is_ipv6() {
  302. // in this case we can't ping the server for some reason
  303. dev_options.no_dev_server_wait = true;
  304. }
  305. }
  306. Ok(())
  307. }
  308. #[allow(clippy::too_many_arguments)]
  309. fn run_dev(
  310. mut interface: AppInterface,
  311. mut options: Options,
  312. mut dev_options: DevOptions,
  313. tauri_config: ConfigHandle,
  314. device: Option<Device>,
  315. env: Env,
  316. app: &App,
  317. config: &AppleConfig,
  318. noise_level: NoiseLevel,
  319. ) -> Result<()> {
  320. // when running on an actual device we must use the network IP
  321. if options.host.is_some()
  322. || device
  323. .as_ref()
  324. .map(|device| !matches!(device.kind(), DeviceKind::Simulator))
  325. .unwrap_or(false)
  326. {
  327. use_network_address_for_dev_url(&tauri_config, &mut options, &mut dev_options)?;
  328. }
  329. crate::dev::setup(&interface, &mut dev_options, tauri_config.clone())?;
  330. let app_settings = interface.app_settings();
  331. let bin_path = app_settings.app_binary_path(&InterfaceOptions {
  332. debug: !dev_options.release_mode,
  333. target: dev_options.target.clone(),
  334. ..Default::default()
  335. })?;
  336. let out_dir = bin_path.parent().unwrap();
  337. let _lock = flock::open_rw(out_dir.join("lock").with_extension("ios"), "iOS")?;
  338. let set_host = options.host.is_some();
  339. configure_cargo(app, None)?;
  340. let open = options.open;
  341. let exit_on_panic = options.exit_on_panic;
  342. let no_watch = options.no_watch;
  343. interface.mobile_dev(
  344. MobileOptions {
  345. debug: true,
  346. features: options.features,
  347. args: Vec::new(),
  348. config: dev_options.config.clone(),
  349. no_watch: options.no_watch,
  350. },
  351. |options| {
  352. let cli_options = CliOptions {
  353. dev: true,
  354. features: options.features.clone(),
  355. args: options.args.clone(),
  356. noise_level,
  357. vars: Default::default(),
  358. config: dev_options.config.clone(),
  359. };
  360. let _handle = write_options(
  361. &tauri_config.lock().unwrap().as_ref().unwrap().identifier,
  362. cli_options,
  363. )?;
  364. if open {
  365. if !set_host {
  366. log::warn!("{PHYSICAL_IPHONE_DEV_WARNING}");
  367. }
  368. open_and_wait(config, &env)
  369. } else if let Some(device) = &device {
  370. match run(device, options, config, &env) {
  371. Ok(c) => {
  372. crate::dev::wait_dev_process(c.clone(), move |status, reason| {
  373. crate::dev::on_app_exit(status, reason, exit_on_panic, no_watch)
  374. });
  375. Ok(Box::new(c) as Box<dyn DevProcess + Send>)
  376. }
  377. Err(e) => {
  378. crate::dev::kill_before_dev_process();
  379. Err(e)
  380. }
  381. }
  382. } else {
  383. if !set_host {
  384. log::warn!("{PHYSICAL_IPHONE_DEV_WARNING}");
  385. }
  386. open_and_wait(config, &env)
  387. }
  388. },
  389. )
  390. }
  391. fn run(
  392. device: &Device<'_>,
  393. options: MobileOptions,
  394. config: &AppleConfig,
  395. env: &Env,
  396. ) -> crate::Result<DevChild> {
  397. let profile = if options.debug {
  398. Profile::Debug
  399. } else {
  400. Profile::Release
  401. };
  402. device
  403. .run(
  404. config,
  405. env,
  406. NoiseLevel::FranklyQuitePedantic,
  407. false, // do not quit on app exit
  408. profile,
  409. )
  410. .map(DevChild::new)
  411. .map_err(Into::into)
  412. }