1use 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 #[clap(short, long, action = ArgAction::Append, num_args(0..), value_delimiter = ',')]
48 pub features: Vec<String>,
49 #[clap(short, long)]
51 exit_on_panic: bool,
52 #[clap(short, long)]
60 pub config: Vec<ConfigValue>,
61 #[clap(long = "release")]
63 pub release_mode: bool,
64 #[clap(long, env = "TAURI_CLI_NO_DEV_SERVER_WAIT")]
66 pub no_dev_server_wait: bool,
67 #[clap(long)]
69 pub no_watch: bool,
70 #[clap(long)]
72 pub additional_watch_folders: Vec<PathBuf>,
73 #[clap(short, long)]
75 pub open: bool,
76 pub device: Option<String>,
78 #[clap(long)]
80 pub force_ip_prompt: bool,
81 #[clap(long, default_value_t, default_missing_value(""), num_args(0..=1))]
96 pub host: DevHost,
97 #[clap(long)]
99 pub no_dev_server: bool,
100 #[clap(long, env = "TAURI_CLI_PORT")]
102 pub port: Option<u16>,
103 #[clap(last(true))]
107 pub args: Vec<String>,
108 #[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 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 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 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}