Skip to content

Commit 75082cc

Browse files
authored
feat(cli): add mobile run commands, closes #13196 (#14120)
* feat(cli): add mobile run commands, closes #13196 * headers * debug by default * fix android env * implement watcher * clippy * skip ipa build
1 parent 006d592 commit 75082cc

File tree

9 files changed

+397
-35
lines changed

9 files changed

+397
-35
lines changed

.changes/mobile-run.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@tauri-apps/cli": minor:feat
3+
"tauri-cli": minor:feat
4+
---
5+
6+
Added `ios run` and `android run` commands to run the app in production mode.

crates/tauri-cli/src/interface/mod.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ use std::{
1414
use crate::{error::Context, helpers::config::Config};
1515
use tauri_bundler::bundle::{PackageType, Settings, SettingsBuilder};
1616

17-
pub use rust::{MobileOptions, Options, Rust as AppInterface};
17+
pub use rust::{MobileOptions, Options, Rust as AppInterface, WatcherOptions};
1818

1919
pub trait DevProcess {
2020
fn kill(&self) -> std::io::Result<()>;
@@ -113,4 +113,9 @@ pub trait Interface: Sized {
113113
options: MobileOptions,
114114
runner: R,
115115
) -> crate::Result<()>;
116+
fn watch<R: Fn() -> crate::Result<Box<dyn DevProcess + Send>>>(
117+
&mut self,
118+
options: WatcherOptions,
119+
runner: R,
120+
) -> crate::Result<()>;
116121
}

crates/tauri-cli/src/interface/rust.rs

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,12 @@ pub struct MobileOptions {
115115
pub additional_watch_folders: Vec<PathBuf>,
116116
}
117117

118+
#[derive(Debug, Clone)]
119+
pub struct WatcherOptions {
120+
pub config: Vec<ConfigValue>,
121+
pub additional_watch_folders: Vec<PathBuf>,
122+
}
123+
118124
#[derive(Debug)]
119125
pub struct RustupTarget {
120126
name: String,
@@ -245,12 +251,26 @@ impl Interface for Rust {
245251
runner(options)?;
246252
Ok(())
247253
} else {
248-
let merge_configs = options.config.iter().map(|c| &c.0).collect::<Vec<_>>();
249-
let run = Arc::new(|_rust: &mut Rust| runner(options.clone()));
250-
self.run_dev_watcher(&options.additional_watch_folders, &merge_configs, run)
254+
self.watch(
255+
WatcherOptions {
256+
config: options.config.clone(),
257+
additional_watch_folders: options.additional_watch_folders.clone(),
258+
},
259+
move || runner(options.clone()),
260+
)
251261
}
252262
}
253263

264+
fn watch<R: Fn() -> crate::Result<Box<dyn DevProcess + Send>>>(
265+
&mut self,
266+
options: WatcherOptions,
267+
runner: R,
268+
) -> crate::Result<()> {
269+
let merge_configs = options.config.iter().map(|c| &c.0).collect::<Vec<_>>();
270+
let run = Arc::new(|_rust: &mut Rust| runner());
271+
self.run_dev_watcher(&options.additional_watch_folders, &merge_configs, run)
272+
}
273+
254274
fn env(&self) -> HashMap<&str, String> {
255275
let mut env = HashMap::new();
256276
env.insert(

crates/tauri-cli/src/mobile/android/build.rs

Lines changed: 35 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ use crate::{
1515
flock,
1616
},
1717
interface::{AppInterface, Interface, Options as InterfaceOptions},
18-
mobile::{write_options, CliOptions},
18+
mobile::{write_options, CliOptions, TargetDevice},
1919
ConfigValue, Error, Result,
2020
};
2121
use clap::{ArgAction, Parser};
@@ -63,10 +63,10 @@ pub struct Options {
6363
pub split_per_abi: bool,
6464
/// Build APKs.
6565
#[clap(long)]
66-
pub apk: bool,
66+
pub apk: Option<bool>,
6767
/// Build AABs.
6868
#[clap(long)]
69-
pub aab: bool,
69+
pub aab: Option<bool>,
7070
/// Open Android Studio
7171
#[clap(short, long)]
7272
pub open: bool,
@@ -83,6 +83,9 @@ pub struct Options {
8383
/// Only use this when you are sure the mismatch is incorrectly detected as version mismatched Tauri packages can lead to unknown behavior.
8484
#[clap(long)]
8585
pub ignore_version_mismatches: bool,
86+
/// Target device of this build
87+
#[clap(skip)]
88+
pub target_device: Option<TargetDevice>,
8689
}
8790

8891
impl From<Options> for BuildOptions {
@@ -104,7 +107,15 @@ impl From<Options> for BuildOptions {
104107
}
105108
}
106109

107-
pub fn command(options: Options, noise_level: NoiseLevel) -> Result<()> {
110+
pub struct BuiltApplication {
111+
pub config: AndroidConfig,
112+
pub interface: AppInterface,
113+
// prevent drop
114+
#[allow(dead_code)]
115+
options_handle: OptionsHandle,
116+
}
117+
118+
pub fn command(options: Options, noise_level: NoiseLevel) -> Result<BuiltApplication> {
108119
crate::helpers::app_paths::resolve();
109120

110121
delete_codegen_vars();
@@ -188,8 +199,8 @@ pub fn command(options: Options, noise_level: NoiseLevel) -> Result<()> {
188199
.context("failed to build Android app")?;
189200

190201
let open = options.open;
191-
let _handle = run_build(
192-
interface,
202+
let options_handle = run_build(
203+
&interface,
193204
options,
194205
build_options,
195206
tauri_config,
@@ -203,12 +214,16 @@ pub fn command(options: Options, noise_level: NoiseLevel) -> Result<()> {
203214
open_and_wait(&config, &env);
204215
}
205216

206-
Ok(())
217+
Ok(BuiltApplication {
218+
config,
219+
interface,
220+
options_handle,
221+
})
207222
}
208223

209224
#[allow(clippy::too_many_arguments)]
210225
fn run_build(
211-
interface: AppInterface,
226+
interface: &AppInterface,
212227
mut options: Options,
213228
build_options: BuildOptions,
214229
tauri_config: ConfigHandle,
@@ -217,10 +232,10 @@ fn run_build(
217232
env: &mut Env,
218233
noise_level: NoiseLevel,
219234
) -> Result<OptionsHandle> {
220-
if !(options.apk || options.aab) {
235+
if !(options.apk.is_some() || options.aab.is_some()) {
221236
// if the user didn't specify the format to build, we'll do both
222-
options.apk = true;
223-
options.aab = true;
237+
options.apk = Some(true);
238+
options.aab = Some(true);
224239
}
225240

226241
let interface_options = InterfaceOptions {
@@ -241,13 +256,13 @@ fn run_build(
241256
noise_level,
242257
vars: Default::default(),
243258
config: build_options.config,
244-
target_device: None,
259+
target_device: options.target_device.clone(),
245260
};
246261
let handle = write_options(tauri_config.lock().unwrap().as_ref().unwrap(), cli_options)?;
247262

248263
inject_resources(config, tauri_config.lock().unwrap().as_ref().unwrap())?;
249264

250-
let apk_outputs = if options.apk {
265+
let apk_outputs = if options.apk.unwrap_or_default() {
251266
apk::build(
252267
config,
253268
env,
@@ -261,7 +276,7 @@ fn run_build(
261276
Vec::new()
262277
};
263278

264-
let aab_outputs = if options.aab {
279+
let aab_outputs = if options.aab.unwrap_or_default() {
265280
aab::build(
266281
config,
267282
env,
@@ -275,8 +290,12 @@ fn run_build(
275290
Vec::new()
276291
};
277292

278-
log_finished(apk_outputs, "APK");
279-
log_finished(aab_outputs, "AAB");
293+
if !apk_outputs.is_empty() {
294+
log_finished(apk_outputs, "APK");
295+
}
296+
if !aab_outputs.is_empty() {
297+
log_finished(aab_outputs, "AAB");
298+
}
280299

281300
Ok(handle)
282301
}

crates/tauri-cli/src/mobile/android/mod.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ mod android_studio_script;
4444
mod build;
4545
mod dev;
4646
pub(crate) mod project;
47+
mod run;
4748

4849
const NDK_VERSION: &str = "29.0.13846066";
4950
const SDK_VERSION: u8 = 36;
@@ -96,6 +97,7 @@ enum Commands {
9697
Init(InitOptions),
9798
Dev(dev::Options),
9899
Build(build::Options),
100+
Run(run::Options),
99101
#[clap(hide(true))]
100102
AndroidStudioScript(android_studio_script::Options),
101103
}
@@ -114,7 +116,8 @@ pub fn command(cli: Cli, verbosity: u8) -> Result<()> {
114116
)?
115117
}
116118
Commands::Dev(options) => dev::command(options, noise_level)?,
117-
Commands::Build(options) => build::command(options, noise_level)?,
119+
Commands::Build(options) => build::command(options, noise_level).map(|_| ())?,
120+
Commands::Run(options) => run::command(options, noise_level)?,
118121
Commands::AndroidStudioScript(options) => android_studio_script::command(options)?,
119122
}
120123

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
2+
// SPDX-License-Identifier: Apache-2.0
3+
// SPDX-License-Identifier: MIT
4+
5+
use cargo_mobile2::{
6+
android::target::Target,
7+
opts::{FilterLevel, NoiseLevel, Profile},
8+
target::TargetTrait,
9+
};
10+
use clap::{ArgAction, Parser};
11+
use std::path::PathBuf;
12+
13+
use super::{configure_cargo, device_prompt, env};
14+
use crate::{
15+
error::Context,
16+
interface::{DevProcess, Interface, WatcherOptions},
17+
mobile::{DevChild, TargetDevice},
18+
ConfigValue, Result,
19+
};
20+
21+
#[derive(Debug, Clone, Parser)]
22+
#[clap(
23+
about = "Run your app in production mode on Android",
24+
long_about = "Run your app in production mode on Android. It makes use of the `build.frontendDist` property from your `tauri.conf.json` file. It also runs your `build.beforeBuildCommand` which usually builds your frontend into `build.frontendDist`."
25+
)]
26+
pub struct Options {
27+
/// Run the app in release mode
28+
#[clap(short, long)]
29+
pub release: bool,
30+
/// List of cargo features to activate
31+
#[clap(short, long, action = ArgAction::Append, num_args(0..))]
32+
pub features: Option<Vec<String>>,
33+
/// JSON strings or paths to JSON, JSON5 or TOML files to merge with the default configuration file
34+
///
35+
/// Configurations are merged in the order they are provided, which means a particular value overwrites previous values when a config key-value pair conflicts.
36+
///
37+
/// Note that a platform-specific file is looked up and merged with the default file by default
38+
/// (tauri.macos.conf.json, tauri.linux.conf.json, tauri.windows.conf.json, tauri.android.conf.json and tauri.ios.conf.json)
39+
/// but you can use this for more specific use cases such as different build flavors.
40+
#[clap(short, long)]
41+
pub config: Vec<ConfigValue>,
42+
/// Disable the file watcher
43+
#[clap(long)]
44+
pub no_watch: bool,
45+
/// Additional paths to watch for changes.
46+
#[clap(long)]
47+
pub additional_watch_folders: Vec<PathBuf>,
48+
/// Open Android Studio
49+
#[clap(short, long)]
50+
pub open: bool,
51+
/// Runs on the given device name
52+
pub device: Option<String>,
53+
/// Command line arguments passed to the runner.
54+
/// Use `--` to explicitly mark the start of the arguments.
55+
/// e.g. `tauri android build -- [runnerArgs]`.
56+
#[clap(last(true))]
57+
pub args: Vec<String>,
58+
/// Do not error out if a version mismatch is detected on a Tauri package.
59+
///
60+
/// Only use this when you are sure the mismatch is incorrectly detected as version mismatched Tauri packages can lead to unknown behavior.
61+
#[clap(long)]
62+
pub ignore_version_mismatches: bool,
63+
}
64+
65+
pub fn command(options: Options, noise_level: NoiseLevel) -> Result<()> {
66+
let mut env = env(false)?;
67+
68+
let device = if options.open {
69+
None
70+
} else {
71+
match device_prompt(&env, options.device.as_deref()) {
72+
Ok(d) => Some(d),
73+
Err(e) => {
74+
log::error!("{e}");
75+
None
76+
}
77+
}
78+
};
79+
80+
let mut built_application = super::build::command(
81+
super::build::Options {
82+
debug: !options.release,
83+
targets: device.as_ref().map(|d| {
84+
vec![Target::all()
85+
.iter()
86+
.find(|(_key, t)| t.arch == d.target().arch)
87+
.map(|(key, _t)| key.to_string())
88+
.expect("Target not found")]
89+
}),
90+
features: options.features,
91+
config: options.config.clone(),
92+
split_per_abi: true,
93+
apk: Some(false),
94+
aab: Some(false),
95+
open: options.open,
96+
ci: false,
97+
args: options.args,
98+
ignore_version_mismatches: options.ignore_version_mismatches,
99+
target_device: device.as_ref().map(|d| TargetDevice {
100+
id: d.serial_no().to_string(),
101+
name: d.name().to_string(),
102+
}),
103+
},
104+
noise_level,
105+
)?;
106+
107+
configure_cargo(&mut env, &built_application.config)?;
108+
109+
// options.open is handled by the build command
110+
// so all we need to do here is run the app on the selected device
111+
if let Some(device) = device {
112+
let config = built_application.config.clone();
113+
let release = options.release;
114+
let runner = move || {
115+
device
116+
.run(
117+
&config,
118+
&env,
119+
noise_level,
120+
if !release {
121+
Profile::Debug
122+
} else {
123+
Profile::Release
124+
},
125+
Some(match noise_level {
126+
NoiseLevel::Polite => FilterLevel::Info,
127+
NoiseLevel::LoudAndProud => FilterLevel::Debug,
128+
NoiseLevel::FranklyQuitePedantic => FilterLevel::Verbose,
129+
}),
130+
false,
131+
false,
132+
".MainActivity".into(),
133+
)
134+
.map(|c| Box::new(DevChild::new(c)) as Box<dyn DevProcess + Send>)
135+
.context("failed to run Android app")
136+
};
137+
138+
if options.no_watch {
139+
runner()?;
140+
} else {
141+
built_application.interface.watch(
142+
WatcherOptions {
143+
config: options.config,
144+
additional_watch_folders: options.additional_watch_folders,
145+
},
146+
runner,
147+
)?;
148+
}
149+
}
150+
151+
Ok(())
152+
}

0 commit comments

Comments
 (0)