Skip to main content

tauri_cli/mobile/android/
dev.rs

1// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
2// SPDX-License-Identifier: Apache-2.0
3// SPDX-License-Identifier: MIT
4
5use super::{
6  configure_cargo, delete_codegen_vars, device_prompt, ensure_init, env, get_app, get_config,
7  inject_resources, open_and_wait, sync_debug_application_id_suffix, MobileTarget,
8};
9use crate::{
10  dev::Options as DevOptions,
11  error::{Context, ErrorExt},
12  helpers::{
13    app_paths::Dirs,
14    config::{get_config as get_tauri_config, ConfigMetadata},
15    flock,
16  },
17  interface::{AppInterface, MobileOptions, Options as InterfaceOptions},
18  mobile::{
19    android::generate_tauri_properties, use_network_address_for_dev_url, write_options, CliOptions,
20    DevChild, DevHost, DevProcess, TargetDevice,
21  },
22  ConfigValue, Error, Result,
23};
24use clap::{ArgAction, Parser};
25
26use cargo_mobile2::{
27  android::{
28    config::{Config as AndroidConfig, Metadata as AndroidMetadata},
29    device::Device,
30    env::Env,
31    target::Target,
32  },
33  opts::{FilterLevel, NoiseLevel, Profile},
34  target::TargetTrait,
35};
36use url::Host;
37
38use std::{env::set_current_dir, net::Ipv4Addr, path::PathBuf};
39
40#[derive(Debug, Clone, Parser)]
41#[clap(
42  about = "Run your app in development mode on Android",
43  long_about = "Run your app in development mode on Android 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."
44)]
45pub struct Options {
46  /// List of cargo features to activate
47  #[clap(short, long, action = ArgAction::Append, num_args(0..), value_delimiter = ',')]
48  pub features: Vec<String>,
49  /// Exit on panic
50  #[clap(short, long)]
51  exit_on_panic: bool,
52  /// JSON strings or paths to JSON, JSON5 or TOML files to merge with the default configuration file
53  ///
54  /// Configurations are merged in the order they are provided, which means a particular value overwrites previous values when a config key-value pair conflicts.
55  ///
56  /// Note that a platform-specific file is looked up and merged with the default file by default
57  /// (tauri.macos.conf.json, tauri.linux.conf.json, tauri.windows.conf.json, tauri.android.conf.json and tauri.ios.conf.json)
58  /// but you can use this for more specific use cases such as different build flavors.
59  #[clap(short, long)]
60  pub config: Vec<ConfigValue>,
61  /// Run the code in release mode
62  #[clap(long = "release")]
63  pub release_mode: bool,
64  /// Skip waiting for the frontend dev server to start before building the tauri application.
65  #[clap(long, env = "TAURI_CLI_NO_DEV_SERVER_WAIT")]
66  pub no_dev_server_wait: bool,
67  /// Disable the file watcher
68  #[clap(long)]
69  pub no_watch: bool,
70  /// Additional paths to watch for changes.
71  #[clap(long)]
72  pub additional_watch_folders: Vec<PathBuf>,
73  /// Open Android Studio instead of trying to run on a connected device
74  #[clap(short, long)]
75  pub open: bool,
76  /// Runs on the given device name
77  pub device: Option<String>,
78  /// Force prompting for an IP to use to connect to the dev server on mobile.
79  #[clap(long)]
80  pub force_ip_prompt: bool,
81  /// Use the public network address for the development server.
82  /// If an actual address it provided, it is used instead of prompting to pick one.
83  ///
84  /// On Windows we use the public network address by default.
85  ///
86  /// This option is particularly useful along the `--open` flag when you intend on running on a physical device.
87  ///
88  /// This replaces the devUrl configuration value to match the public network address host,
89  /// it is your responsibility to set up your development server to listen on this address
90  /// by using 0.0.0.0 as host for instance.
91  ///
92  /// When this is set or when running on an iOS device the CLI sets the `TAURI_DEV_HOST`
93  /// environment variable so you can check this on your framework's configuration to expose the development server
94  /// on the public network address.
95  #[clap(long, default_value_t, default_missing_value(""), num_args(0..=1))]
96  pub host: DevHost,
97  /// Disable the built-in dev server for static files.
98  #[clap(long)]
99  pub no_dev_server: bool,
100  /// Specify port for the built-in dev server for static files. Defaults to 1430.
101  #[clap(long, env = "TAURI_CLI_PORT")]
102  pub port: Option<u16>,
103  /// Command line arguments passed to the runner.
104  /// Use `--` to explicitly mark the start of the arguments.
105  /// e.g. `tauri android dev -- [runnerArgs]`.
106  #[clap(last(true))]
107  pub args: Vec<String>,
108  /// Path to the certificate file used by your dev server. Required for mobile dev when using HTTPS.
109  #[clap(long, env = "TAURI_DEV_ROOT_CERTIFICATE_PATH")]
110  pub root_certificate_path: Option<PathBuf>,
111}
112
113impl From<Options> for DevOptions {
114  fn from(options: Options) -> Self {
115    Self {
116      runner: None,
117      target: None,
118      features: options.features,
119      exit_on_panic: options.exit_on_panic,
120      config: options.config,
121      args: options.args,
122      no_watch: options.no_watch,
123      additional_watch_folders: options.additional_watch_folders,
124      no_dev_server_wait: options.no_dev_server_wait,
125      no_dev_server: options.no_dev_server,
126      port: options.port,
127      release_mode: options.release_mode,
128      host: options.host.0.unwrap_or_default(),
129    }
130  }
131}
132
133pub fn command(options: Options, noise_level: NoiseLevel) -> Result<()> {
134  let dirs = crate::helpers::app_paths::resolve_dirs();
135
136  let result = run_command(options, noise_level, dirs);
137  if result.is_err() {
138    crate::dev::kill_before_dev_process();
139  }
140  result
141}
142
143fn run_command(options: Options, noise_level: NoiseLevel, dirs: Dirs) -> Result<()> {
144  delete_codegen_vars();
145  // setup env additions before calling env()
146  if let Some(root_certificate_path) = &options.root_certificate_path {
147    std::env::set_var(
148      "TAURI_DEV_ROOT_CERTIFICATE",
149      std::fs::read_to_string(root_certificate_path).fs_context(
150        "failed to read certificate file",
151        root_certificate_path.clone(),
152      )?,
153    );
154  }
155
156  let tauri_config = get_tauri_config(
157    tauri_utils::platform::Target::Android,
158    &options
159      .config
160      .iter()
161      .map(|conf| &conf.0)
162      .collect::<Vec<_>>(),
163    dirs.tauri,
164  )?;
165
166  let env = env(false)?;
167  let device = if options.open {
168    None
169  } else {
170    match device_prompt(&env, options.device.as_deref()) {
171      Ok(d) => Some(d),
172      Err(e) => {
173        log::error!("{e}");
174        None
175      }
176    }
177  };
178
179  let mut dev_options: DevOptions = options.clone().into();
180  let target_triple = device
181    .as_ref()
182    .map(|d| d.target().triple.to_string())
183    .unwrap_or_else(|| Target::all().values().next().unwrap().triple.into());
184  dev_options.target = Some(target_triple);
185  dev_options.args.push("--lib".into());
186
187  let interface = AppInterface::new(&tauri_config, dev_options.target.clone(), dirs.tauri)?;
188
189  let app = get_app(MobileTarget::Android, &tauri_config, &interface, dirs.tauri);
190  let (config, metadata) = get_config(
191    &app,
192    &tauri_config,
193    dev_options.features.as_ref(),
194    &CliOptions {
195      dev: true,
196      features: dev_options.features.clone(),
197      args: dev_options.args.clone(),
198      noise_level,
199      vars: Default::default(),
200      config: dev_options.config.clone(),
201      target_device: None,
202    },
203  );
204
205  set_current_dir(dirs.tauri).context("failed to set current directory to Tauri directory")?;
206
207  ensure_init(
208    &tauri_config,
209    config.app(),
210    config.project_dir(),
211    MobileTarget::Android,
212    false,
213  )?;
214  run_dev(
215    interface,
216    options,
217    dev_options,
218    tauri_config,
219    device,
220    env,
221    &config,
222    &metadata,
223    noise_level,
224    &dirs,
225  )
226}
227
228#[allow(clippy::too_many_arguments)]
229fn run_dev(
230  mut interface: AppInterface,
231  options: Options,
232  mut dev_options: DevOptions,
233  mut tauri_config: ConfigMetadata,
234  device: Option<Device>,
235  mut env: Env,
236  config: &AndroidConfig,
237  metadata: &AndroidMetadata,
238  noise_level: NoiseLevel,
239  dirs: &Dirs,
240) -> Result<()> {
241  // when --host is provided or running on a physical device or resolving 0.0.0.0 we must use the network IP
242  if options.host.0.is_some()
243    || device
244      .as_ref()
245      .map(|device| !device.serial_no().starts_with("emulator"))
246      .unwrap_or(false)
247    || tauri_config.build.dev_url.as_ref().is_some_and(|url| {
248      matches!(
249        url.host(),
250        Some(Host::Ipv4(i)) if i == Ipv4Addr::UNSPECIFIED
251      )
252    })
253  {
254    use_network_address_for_dev_url(
255      &mut tauri_config,
256      &mut dev_options,
257      options.force_ip_prompt,
258      dirs.tauri,
259    )?;
260  }
261
262  crate::dev::setup(&interface, &mut dev_options, &mut tauri_config, dirs)?;
263
264  let interface_options = InterfaceOptions {
265    debug: !dev_options.release_mode,
266    target: dev_options.target.clone(),
267    ..Default::default()
268  };
269
270  let app_settings = interface.app_settings();
271  let out_dir = app_settings.out_dir(&interface_options, dirs.tauri)?;
272  let _lock = flock::open_rw(out_dir.join("lock").with_extension("android"), "Android")?;
273
274  configure_cargo(&mut env, config)?;
275
276  generate_tauri_properties(config, &tauri_config, true)?;
277  sync_debug_application_id_suffix(config, &tauri_config)?;
278
279  let installed_targets =
280    crate::interface::rust::installation::installed_targets().unwrap_or_default();
281
282  // run an initial build to initialize plugins
283  let target_triple = dev_options.target.as_ref().unwrap();
284  let target = Target::all()
285    .values()
286    .find(|t| t.triple == target_triple)
287    .unwrap_or_else(|| Target::all().values().next().unwrap());
288  if !installed_targets.contains(&target.triple().into()) {
289    log::info!("Installing target {}", target.triple());
290    target.install().map_err(|error| Error::CommandFailed {
291      command: "rustup target add".to_string(),
292      error,
293    })?;
294  }
295
296  target
297    .build(
298      config,
299      metadata,
300      &env,
301      noise_level,
302      true,
303      if options.release_mode {
304        Profile::Release
305      } else {
306        Profile::Debug
307      },
308    )
309    .context("failed to build Android app")?;
310
311  let open = options.open;
312  interface.mobile_dev(
313    &mut tauri_config,
314    MobileOptions {
315      debug: !options.release_mode,
316      features: options.features,
317      args: options.args,
318      config: dev_options.config.clone(),
319      no_watch: options.no_watch,
320      additional_watch_folders: options.additional_watch_folders,
321    },
322    |options, tauri_config| {
323      let cli_options = CliOptions {
324        dev: true,
325        features: options.features.clone(),
326        args: options.args.clone(),
327        noise_level,
328        vars: Default::default(),
329        config: dev_options.config.clone(),
330        target_device: device.as_ref().map(|d| TargetDevice {
331          id: d.serial_no().to_string(),
332          name: d.name().to_string(),
333        }),
334      };
335
336      let _handle = write_options(tauri_config, cli_options)?;
337
338      inject_resources(config, tauri_config)?;
339
340      if open {
341        open_and_wait(config, &env)
342      } else if let Some(device) = &device {
343        match run(
344          device,
345          options,
346          config,
347          &env,
348          metadata,
349          noise_level,
350          tauri_config,
351        ) {
352          Ok(c) => Ok(Box::new(c) as Box<dyn DevProcess + Send>),
353          Err(e) => {
354            crate::dev::kill_before_dev_process();
355            Err(e)
356          }
357        }
358      } else {
359        open_and_wait(config, &env)
360      }
361    },
362    dirs,
363  )
364}
365
366fn run(
367  device: &Device<'_>,
368  options: MobileOptions,
369  config: &AndroidConfig,
370  env: &Env,
371  metadata: &AndroidMetadata,
372  noise_level: NoiseLevel,
373  tauri_config: &tauri_utils::config::Config,
374) -> crate::Result<DevChild> {
375  let profile = if options.debug {
376    Profile::Debug
377  } else {
378    Profile::Release
379  };
380
381  let build_app_bundle = metadata.asset_packs().is_some();
382
383  let application_id_suffix = if profile == Profile::Debug {
384    tauri_config
385      .bundle
386      .android
387      .debug_application_id_suffix
388      .clone()
389  } else {
390    None
391  };
392
393  device
394    .run_with_application_id_suffix(
395      config,
396      env,
397      noise_level,
398      profile,
399      Some(match noise_level {
400        NoiseLevel::Polite => FilterLevel::Info,
401        NoiseLevel::LoudAndProud => FilterLevel::Debug,
402        NoiseLevel::FranklyQuitePedantic => FilterLevel::Verbose,
403      }),
404      build_app_bundle,
405      false,
406      format!("{}.MainActivity", config.app().identifier()),
407      application_id_suffix,
408    )
409    .map(DevChild::new)
410    .context("failed to run Android app")
411}